[
  {
    "path": ".gitattributes",
    "content": "/tests          export-ignore\n/.github        export-ignore\n/phpcs.xml      export-ignore\n/phpunit.xml    export-ignore\n/phpbench.json  export-ignore\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: Tests\n\non:\n  push:\n    branches:\n      - 2.x\n      - 3.x\n      - 4.x\n      - develop\n  pull_request:\n\njobs:\n  phpUnitTests:\n    runs-on: ubuntu-latest\n    strategy:\n      max-parallel: 6\n      matrix:\n        phpVersions: ['8.2', '8.3', '8.4', '8.5']\n      fail-fast: false\n    name: PHP ${{ matrix.phpVersions }}\n    steps:\n      - name: Checkout changes\n        uses: actions/checkout@v1\n\n      - name: Install PHP\n        uses: shivammathur/setup-php@master\n        with:\n          php-version: ${{ matrix.phpVersions }}\n\n      - name: Install Composer dependencies\n        run: |\n            composer config --no-interaction allow-plugins.composer/installers true\n            composer install --no-interaction --no-progress --no-scripts\n\n      - name: Run Tests\n        run: ./vendor/bin/phpunit ./tests\n"
  },
  {
    "path": ".gitignore",
    "content": "# Composer files\n/vendor\ncomposer.phar\ncomposer.lock\n\n# Editor files\n.idea\n.vscode\n.claude\n\n# Other files\n.DS_Store\nphp_errors.log\n.phpunit.result.cache\n"
  },
  {
    "path": "CREDITS.md",
    "content": "# Credits\n\nThis library was created with help from the following packages:\n\n\"Laravel\", Copyright (c) Taylor Otwell\nhttps://github.com/laravel/framework\n\n\"Parsedown\", Copyright (c) 2013-2018 Emanuil Rusev, erusev.com\nhttps://github.com/erusev/parsedown\n\n\"Assetic\", Copyright (c) 2010-2015 OpenSky Project Inc\nhttps://github.com/kriswallsmith/assetic\n\n\"http_build_url() for PHP\", Copyright (c) 2015 Jake A. Smith\nhttps://github.com/jakeasmith/http_build_url\n\n\"Twig extensions\", Copyright (c) 2016 Vojta Svoboda\nhttps://github.com/vojtasvoboda/oc-twigextensions-plugin\n\n\"October Code\", Copyright (c) 2022 Sergey Kasyanov\nhttps://github.com/SergeyKasyanov/vscode-october-extension\n"
  },
  {
    "path": "LICENSE.md",
    "content": "Copyright (c) 2013-2022 Responsiv Pty Ltd\n\nThis End User License Agreement (“EULA”) constitutes a binding agreement between you (the “Licensee”, “you” or “your”) and Responsiv Pty Ltd - ACN 159 492 823 (the “Company”, “we”, “us” or “our”) with respect to your use of the October CMS software (“Licensed Software” or “October CMS Software”). The Company and the Licensee are each individually referred to as “Party” and collectively as “Parties”.\n\nPlease carefully read the terms and conditions of this EULA before installing and using the Licensed Software. By using the Licensed Software, you represent that you have read this EULA, and you agree to be bound by all the terms and conditions of this EULA, including any other agreements and policies referenced in this EULA. If you do not agree with any provisions of this EULA, please do not install the October CMS Software.\n\nThe Company reserves the right to modify or discontinue the October CMS Software or any portion thereof, temporarily or permanently, with or without notice to you. The Company will not be under any obligation to support or update the Licensed Software, except as described in this EULA.\n\nYOU AGREE THAT THE COMPANY SHALL NOT BE LIABLE TO YOU OR ANY THIRD PARTY IN THE EVENT THAT WE EXERCISE OUR RIGHT TO MODIFY OR DISCONTINUE THE LICENSED SOFTWARE OR ANY PORTION THEREOF.\n\n## Summary\n\nThis section outlines some of the key provisions covered in this EULA. Please note that this summary is provided for your convenience only, and it does not relieve you of your obligation to read the full EULA before installing/using the October CMS Software.\n\nBy proceeding to use the October CMS Software, you understand and agree that:\n\n- You must be at least 18 years of age to enter into this EULA;\n\n- You will only use the October CMS Software in compliance with applicable laws;\n\n- October CMS Software licenses are only issued for Projects created through the website. To acquire/renew your Project licence, you need to sign in to your October CMS Account, and select/create the Project for which you wish to acquire/renew the licence;\n\n- You will be responsible for paying the full License Fee prior to installing the October CMS Software;\n\n- All License Fee Payments are non-refundable;\n\n- Upon full payment of the License Fee, you will receive a License Key that allows you to install the Licensed Software to create a single production or non-production website and ancillary installations needed to support that single production or non-production website;\n\n- Each new/renewed Project licence comes with one year of Updates. You may continue to use your expired Project license in perpetuity, but if you wish to continue receiving all the Updates, you will be required to keep your Project licence active by renewing it every year;\n\n- Subject to the payment of full License Fee and compliance with this EULA, the Company and its third party licensors grant you a limited, non-exclusive, non-transferable, non-assignable, perpetual and worldwide license to install and/or use the October CMS Software;\n\n- The Company and its licensors retain all rights, title and interest in the October CMS Software, Documentation and other similar proprietary materials made available to you;\n\n- You will not sublicense, resell, distribute, or transfer the Licensed Software to any third party without the Company’s prior written consent;\n\n- You will not remove, obscure or otherwise modify any copyright notices from the October CMS Software files or this EULA;\n\n- You will not modify, disassemble, or reverse engineer any part of the October CMS Software;\n\n- You will take all required steps to prevent unauthorised installation/use of the October CMS Software and prevent any breach of this EULA;\n\n- The Company may terminate this EULA if you are in breach of any provision of this EULA or if we discontinue the October CMS Software;\n\n- SUBJECT TO YOUR STATUTORY RIGHTS, THE COMPANY PROVIDES THE OCTOBER CMS SOFTWARE TO YOU “AS IS” AND “WITH ALL FAULTS”. THE COMPANY DOES NOT OFFER ANY WARRANTIES, WHETHER EXPRESS OR IMPLIED. IN NO EVENT WILL THE COMPANY’S AGGREGATE LIABILITY TO YOU FOR ANY CLAIMS CONNECTED WITH THIS EULA OR THE OCTOBER CMS SOFTWARE EXCEED THE AMOUNT ACTUALLY PAID BY YOU TO THE COMPANY FOR THE OCTOBER CMS SOFTWARE IN THE TWELVE MONTHS PRECEDING THE DATE WHEN THE CLAIM FIRST AROSE;\n\n- This EULA shall be governed by and construed in accordance with the laws of the state of New South Wales, Australia, without giving effect to any principles of conflict of laws.\n\n## Table of Contents\n\n- [1. Definitions](#Definitions)\n- [2. Authorisation](#Authorisation)\n- [3. License Grant](#License-Grant)\n- [4. License Purchase and Renewal](#License-Purchase-and-Renewal)\n- [5. License Fees, Payments and Refunds](#License-Fees-Payments-and-Refunds)\n- [6. Ownership](#Ownership)\n- [7. Use and Restrictions](#Use-and-Restrictions)\n- [8. Technical Support](#Technical-Support)\n- [9. Termination](#Termination)\n- [10. Statutory Consumer Rights](#Statutory-Consumer-Rights)\n- [11. Disclaimer of Warranties](#Disclaimer-of-Warranties)\n- [12. Limitation of Liability](#Limitation-of-Liability)\n- [13. Waiver](#Waiver)\n- [14. Indemnification](#Indemnification)\n- [15. Compliance with the Laws](#Compliance-with-the-Laws)\n- [16. Data Protection](#Data-Protection)\n- [17. Delivery](#Delivery)\n- [18. General](#General)\n\n<a name=\"Definitions\"></a>\n## 1. Definitions\n\nThe following words shall have the meaning given hereunder whenever they appear in this EULA:\n\nTerm | Definition\n---- | -----------\nAccount | refers to a user account registered on the Website by an individual or entity.\nActive Term | means the period commencing from the date of License purchase/renewal until the License expiry date as specified on the Project page in your Account.\nDocumentation | means the current version of the documentation relating to the October CMS Software available at https://docs.octobercms.com or otherwise made available by the Company in electronic format. Documentation includes but is not limited to installation instructions, user guides and help documents regarding the use of the October CMS Software.\nLicense Fee | means the fee payable by the Licensee for the use of the October CMS Software in accordance with the provisions of this EULA and any other applicable agreements referenced in this EULA.\nLicense Key | means a unique string of characters provided by the Company that enables users to install the October CMS Software for a specific Project.\nModifications | means any changes to the original code or files of the October CMS Software by the Licensee or any third party. For the avoidance of any doubt, the term “Modifications” does not include any Updates furnished by the Company.\nOctober CMS Software | refers to the collection of proprietary code and graphics in various file formats provided by the Company to the Licensee.\nProject | means a collection of extensions and themes to be used in a single production website.\nUpdates | means all new releases of the October CMS Software, including but not limited to new features and fixes which are provided by the Company to a Licensee during the Active Term of the License.\nThe Website | refers to the Company website located at www.octobercms.com.\n\n<a name=\"Authorisation\"></a>\n## 2. Authorisation\n\nYou must be at least 18 years of age and have the legal capacity to enter into this EULA. If you are accepting this EULA on behalf of a corporation, organisation, or other legal entity (‘corporate entity’), you warrant that you have the authority to enter into this EULA on behalf of such corporate entity and to bind the former to this EULA.\n\n<a name=\"License-Grant\"></a>\n## 3. License Grant\n\nSubject to your compliance with all the terms and conditions of this EULA and the payment of the full License Fee, the Company and its third party licensors grant you a limited, non-exclusive, non-transferable, non-assignable, perpetual and worldwide license to install and/or use the October CMS Software.\n\nYou shall be solely responsible for ensuring that all your authorised employees, contractors or other users of the Licensed Software comply with all the terms and conditions of this EULA, and any failure to comply with this EULA will be deemed as a breach of this EULA by you.\n\nThe Company and its third party licensors may make changes to the Licensed Software, which are provided to you through Updates. You will only receive all such Updates during the Active Term of your license. You will not have any right to receive Updates after the expiration of your license. Please note that if you terminate your Account, you will not be able to access your Projects and renew your license/receive any future Updates.\n\nUNLESS EXPRESSLY PROVIDED IN THIS EULA, YOU MAY NOT COPY OR MODIFY THE OCTOBER CMS SOFTWARE.\n\n<a name=\"License-Purchase-and-Renewal\"></a>\n## 4. License Purchase and Renewal\n\nOctober CMS Software licenses are only issued for Projects created through the Website. To acquire a new Project licence or to renew an existing Project licence, you must sign in to your October CMS Account and select/create the Project for which you wish to acquire/renew the licence.\n\nUpon full payment of the License Fee, you will receive a License Key that allows you to install the Licensed Software to create a single production or non-production website and ancillary installations needed to support that single production or non-production website.\n\nEach new/renewed Project licence comes with one year of Updates starting from the date on which you pay the License Fee. You are not under any legal obligation to renew your Project licence, and you can continue to use your expired Project license for your website in perpetuity. Please note that expired Project licenses do not receive any Updates. If you wish to continue receiving all the Updates, you will be required to keep your Project licence active by renewing it every year.\n\nBy installing and using the October CMS Software, you assume full responsibility for your selection of the Licensed Software, its installation, and the results obtained from the use of the October CMS Software.\n\n<a name=\"License-Fees-Payments-and-Refunds\"></a>\n## 5. License Fees, Payments and Refunds\n\nThe current License Fee for the October CMS Software is published on the Website, and the amount is specified in United States Dollars (USD). The License fee as specified on the Website does not include any taxes which shall be payable by the Licensee in addition to the License Fee.\n\nThe License fee becomes due and payable in full at the time the Licensee purchases a new Project license or renews an existing Project license.\n\nAll Project licenses are issued/renewed subject to the payment of the License Fee by the Licensee as outlined in Section 7 of our [Website Terms of Use](https://octobercms.com/help/terms/website#fees-payments-refunds-policy) and incorporated into this EULA by reference. The Company reserves the right to refuse issuance of a new Project license or renewal of an existing license until the Company receives the full payment.\n\nTo the extent permitted by law, all License Fee payments are non-refundable.\n\n<a name=\"Ownership\"></a>\n## 6. Ownership\n\nNothing in this EULA constitutes the sale of October CMS Software to you. The Company and its licensors retain all rights, title and interest in the October CMS Software, Documentation and other similar proprietary materials made available to you (collectively “Proprietary Material”). All Proprietary Material is protected by copyright and other intellectual property laws of Australia and international conventions. You acknowledge that the October CMS Software may contain some open source software that is not owned by the Company and which shall be governed by its own license terms.\n\nExcept where authorised by the Company in writing, any use of the October CMS trademark, trade name, or logo is strictly prohibited. The Company reserves all rights that are not expressly granted in and to the Proprietary Material.\n\n<a name=\"Use-and-Restrictions\"></a>\n## 7. Use and Restrictions\n\nYou hereby agree that:\n\n1. A License Key may only be used to create a single production or non-production website as provided in Section 4 (License Purchase and Renewal) of this EULA. If you wish to create another website, you will need to create a new Project and acquire a new License for that Project;\n\n2. Unless expressly provided otherwise in this EULA or other applicable agreements referenced herein, you will not sublicense, resell, distribute, or transfer the Licensed Software to any third party without the Company’s prior written consent;\n\n3. You will not remove, obscure or otherwise modify any copyright notices from the October CMS Software files or this EULA;\n\n4. You will not modify, disassemble, or reverse engineer any part of the October CMS Software;\n\n5. You will take all required steps to prevent unauthorised installation/use of the October CMS Software and prevent any breach of this EULA;\n\n6. You do not receive any rights, interests or titles in the “October CMS” trademark, trade name or service mark (“Marks”), and you will not use any of these Marks without our express consent.\n\n<a name=\"Technical-Support\"></a>\n## 8. Technical Support\n\nThe Company does not offer any technical support except as described in our Premium Support Policy. The Company reserves the right to deny any and all technical support for any Modifications of the October CMS Software or in the event of any breach of this EULA.\n\n<a name=\"Termination\"></a>\n## 9. Termination\n\nThis EULA shall remain effective until terminated by either Party as described below.\n\n### 9.1 Termination by Licensee\n\nYou may terminate this EULA by uninstalling the October CMS Software and deleting all files and your Account. Please note that once you delete your Account, you will not be able to reactivate it to restore your Projects and access your License Key.\n\n### 9.2 Termination by the Company\n\nThe Company may terminate this EULA if you are in breach of any provision of this EULA or if we discontinue the October CMS Software.\n\n### 9.3 Survival\n\nSection 11 (Disclaimer of Warranties), Section 12 (Limitation of Liability), Section 13 (Waiver), Section 14 (Indemnification) and other sections of this EULA that by their nature are intended to survive the termination of this EULA shall survive.\n\nUnless expressly specified otherwise in this EULA, any termination of this EULA either by you or the Company does not create any obligation on the Company to issue a full or partial refund of the License Fee paid by you.\n\n<a name=\"Statutory-Consumer-Rights\"></a>\n## 10. Statutory Consumer Rights\n\n### 10.1 CONSUMERS IN AUSTRALIA\n\nIf you acquire the October CMS Software license as a “consumer”, nothing in this EULA will exclude, limit or modify any rights and guarantees conferred on you by legislation, including the Australian Consumer Law (ACL) in the Competition and Consumer Act 2010 (Cth) (‘Statutory Rights’). If the Company is in breach of any such Statutory Rights, then the Company’s liability shall be limited (at the Company’s option) to:\n\n10.1.1 In case of products supplied to you, to resupplying, replacing or paying the cost of resupplying or replacing the product in respect of which the breach occurred;\n\n10.1.2 In case of services supplied to you, to resupply the service or to pay the cost of resupplying the service in respect of which the breach occurred.\n\nUnless expressly specified otherwise in this EULA, you agree that the Company’s liability for the October CMS Software is governed solely by this EULA and the Australian Consumer Law.\n\n### 10.1 CONSUMERS OUTSIDE OF AUSTRALIA\n\nIf you are deemed a “consumer” by statutory law in your country of residence, you may enjoy some legal rights under your local law which prohibit the exclusions, modification or limitations of certain liabilities from applying to you, and where such prohibition exists in your country of residence, any such limitations or exclusions will only apply to you to the extent it is permitted by your local law.\n\nYou agree that apart from the application of your statutory consumer rights, the Company’s liability for the October CMS Software is governed solely by this EULA.\n\n<a name=\"Disclaimer-of-Warranties\"></a>\n## 11. Disclaimer of Warranties\n\nSUBJECT TO YOUR STATUTORY RIGHTS AS PROVIDED IN SECTION 10 ABOVE, THE COMPANY PROVIDES THE OCTOBER CMS SOFTWARE TO YOU “AS IS” AND “WITH ALL FAULTS”.\n\nEXCLUDING ANY EXPRESS WARRANTIES OFFERED IN THIS EULA, TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, THE COMPANY, ITS EMPLOYEES, DIRECTORS, CONTRACTORS, AFFILIATES (“THE COMPANY AND ITS OFFICERS”) DISCLAIM ANY EXPRESS OR IMPLIED WARRANTIES WITH RESPECT TO THE OCTOBER CMS SOFTWARE, INCLUDING WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, ACCURACY, RELIABILITY, COURSE OF PERFORMANCE OR USAGE IN TRADE. THE COMPANY DOES NOT WARRANT THAT THE OCTOBER CMS SOFTWARE: WILL MEET YOUR REQUIREMENTS; WILL BE UNINTERRUPTED, ERROR-FREE, OR SECURE; OR THAT THE COMPANY WILL BE ABLE TO RECTIFY ANY ERRORS, BUGS, SECURITY VULNERABILITIES. NO OBLIGATION, WARRANTIES OR LIABILITY SHALL ARISE OUT OF ANY TECHNICAL SUPPORT SERVICES PROVIDED BY THE COMPANY AND ITS OFFICERS IN CONNECTION WITH THE OCTOBER CMS SOFTWARE. NO VERBAL OR WRITTEN COMMUNICATION RECEIVED FROM THE COMPANY AND ITS OFFICERS, WHETHER MARKETING, PROMOTIONAL OR TECHNICAL SUPPORT, SHALL CREATE ANY WARRANTIES THAT ARE NOT EXPRESSLY PROVIDED IN THIS EULA.\n\nALTHOUGH THE COMPANY PERIODICALLY RELEASES UPDATES FOR THE OCTOBER CMS SOFTWARE THAT MAY INCLUDE FIXES FOR KNOWN VULNERABILITIES, YOU ACKNOWLEDGE AND AGREE THAT THERE MAY BE VULNERABILITIES THAT THE COMPANY HAS NOT YET IDENTIFIED AND THEREFORE CANNOT ADDRESS. YOU ACKNOWLEDGE AND AGREE THAT YOU ARE SOLELY RESPONSIBLE FOR TAKING ALL THE PRECAUTIONS AND SAFEGUARDS NECESSARY TO PROTECT YOUR WEBSITE AND DATA FROM ANY EXTERNAL ATTACKS, INCLUDING BUT NOT LIMITED TO KEEPING YOUR OCTOBER CMS SOFTWARE INSTALLATION CURRENT AND UP TO DATE.\n\nSOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OF IMPLIED WARRANTIES, SO THE ABOVE EXCLUSION MAY NOT APPLY TO YOU.\n\n<a name=\"Limitation-of-Liability\"></a>\n## 12. Limitation of Liability\n\nIN NO EVENT SHALL THE COMPANY BE LIABLE TO YOU OR ANY THIRD-PARTY FOR ANY LOSS OF REVENUE, LOSS OF PROFITS (ACTUAL OR ANTICIPATED), LOSS OF SAVINGS (ACTUAL OR ANTICIPATED), LOSS OF OPPORTUNITY, LOSS OF REPUTATION, LOSS OF GOODWILL OR FOR ANY INDIRECT, INCIDENTAL, CONSEQUENTIAL, SPECIAL OR PUNITIVE DAMAGES RESULTING FROM THE INSTALLATION, USE OR INABILITY TO USE THE OCTOBER CMS SOFTWARE, WHETHER ARISING FROM ANY BREACH OF CONTRACT, NEGLIGENCE OR ANY OTHER THEORY OF LIABILITY, EVEN IF THE COMPANY WAS PREVIOUSLY ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. THE LICENSEE SHALL BE SOLELY RESPONSIBLE FOR DETERMINING THE SUITABILITY OF THE LICENSED SOFTWARE AND ALL RISKS ASSOCIATED WITH ITS USE.\n\nIN NO EVENT WILL THE COMPANY’S AGGREGATE LIABILITY TO YOU FOR ANY CLAIMS CONNECTED WITH THIS EULA OR THE OCTOBER CMS SOFTWARE, INCLUDING THOSE ARISING FROM ANY BREACH OF CONTRACT OR NEGLIGENCE, EXCEED THE AMOUNT ACTUALLY PAID BY YOU TO THE COMPANY FOR THE OCTOBER CMS SOFTWARE IN THE TWELVE MONTHS PRECEDING THE DATE WHEN THE CLAIM FIRST AROSE.\n\nNOTWITHSTANDING ANYTHING TO THE FOREGOING, NOTHING IN THIS PROVISION SHALL LIMIT EITHER PARTY’S LIABILITY FOR ANY FRAUD OR LIABILITY THAT CANNOT BE LIMITED BY LAW.\n\n<a name=\"Waiver\"></a>\n## 13. Waiver\n\nYOU HEREBY RELEASE THE COMPANY AND ITS OFFICERS FROM ALL UNKNOWN RISKS ARISING OUT OF OR ASSOCIATED WITH THE USE OF THE OCTOBER CMS SOFTWARE. IF YOU ARE A RESIDENT IN THE STATE OF CALIFORNIA, U.S.A., YOU EXPRESSLY WAIVE CALIFORNIA CIVIL CODE SECTION 1542, OR OTHER SIMILAR LAW APPLICABLE TO YOU, WHICH STATES: “A GENERAL RELEASE DOES NOT EXTEND TO CLAIMS WHICH THE CREDITOR DOES NOT KNOW OR SUSPECT TO EXIST IN HIS OR HER FAVOR AT THE TIME OF EXECUTING THE RELEASE, WHICH IF KNOWN BY HIM OR HER MUST HAVE MATERIALLY AFFECTED HIS OR HER SETTLEMENT WITH THE DEBTOR OR RELEASED PARTY. ”\n\n**YOU ACKNOWLEDGE AND AGREE THAT THE LIMITATION OF LIABILITY AND THE WAIVER SET FORTH ABOVE REFLECT A REASONABLE AND FAIR ALLOCATION OF RISK BETWEEN YOU AND THE COMPANY AND THAT THESE PROVISIONS FORM AN ESSENTIAL BASIS OF THE BARGAIN BETWEEN YOU AND THE COMPANY. THE COMPANY WOULD NOT BE ABLE TO PROVIDE THE OCTOBER CMS SOFTWARE TO YOU ON AN ECONOMICALLY REASONABLE BASIS WITHOUT THESE LIMITATIONS.**\n\n<a name=\"Indemnification\"></a>\n## 14. Indemnification\n\n14.1 The Company will defend or settle any claims against you that allege that the October CMS Software as supplied to you for installation and use in accordance with this EULA and Documentation infringes the intellectual property rights of a third party provided that:\n\n14.1.1 You immediately notify the Company of any such claim that relates to this indemnity;\n\n14.1.2 The Company has the sole right to control the defence or settlement of such claim; and\n\n14.1.3 You provide the Company with all reasonable assistance in relation to the defence.\n\n14.2 For any claims of infringement of intellectual property mentioned in Section 14.1, the Company reserves the right to:\n\n14.2.1 Modify or replace the Licensed Software to make it non-infringing provided such modification or replacement does not substantively change the functionality of the Licensed Software; or\n\n14.2.2 Acquire at its own expense the right for you to continue the use of the Licensed Software; or\n\n14.2.3 Terminate the Project license and direct you to cease the use of the Licensed Software. In such cases, the Company will provide you with a full refund of the License Fees paid by you for the Licensed Software in the preceding 12 months. Please note that where the Company directs you to cease the use of the October CMS Software due to a third party claim of infringement, you are under a legal obligation to immediately cease such use.\n\nUnless otherwise provided by law, this Section 14 sets out your exclusive remedies for any infringement of intellectual property claims made by a third party against you and nothing in this EULA will create any obligations on the Company to offer greater indemnity. The Company does not have any obligation to defend or indemnify any other third party.\n\n14.3 The Company shall not have any obligation to indemnify you if:\n\n14.3.1 The infringement arises out of any Modification of the October CMS Software;\n\n14.3.2 The infringement arises from any combination, operation, or use of the Licensed Software with any other third party software;\n\n14.3.3 The infringement arises from the use of the Licensed Software in breach of this EULA;\n\n14.3.4 The infringement arises from the use of the Licensed Software after the Company has informed you to cease the use of the Licensed Software due to possible claims.\n\n14.4 You will indemnify the Company against any third party claims, damages, losses and costs, including reasonable attorney fees arising from:\n\n14.4.1 Your actions or omissions (actual or alleged), including without limitation, claims that your actions or omission infringe any third party’s intellectual property rights; or\n\n14.4.2 Your violation of any applicable laws; or\n\n14.4.3 Your breach of this EULA.\n\n<a name=\"Compliance-with-the-Laws\"></a>\n## 15. Compliance with the Laws\n\nYou will only install/use the October CMS Software and fulfil all your obligations under this EULA in compliance with all applicable laws. You hereby confirm that neither you nor the corporate entity that you represent is subject or target of any government Sanctions, and your use of the October CMS Software would not result in violation of any Sanctions by the Company.\n\nYou further confirm that the Licensed Software will not be used by any individual or entity engaged in any of the following activities: (i) Terrorist activities; (ii) design, development or production of any weapons of mass destruction; or (iii) any other illegal activity.\n\n<a name=\"Data-Protection\"></a>\n## 16. Data Protection\n\nThe Company only collects minimal personal data from the Licensee, as described in our Privacy Policy. The Company does not process any data on behalf of the Licensee, and the Licensee shall be solely responsible for compliance with applicable data protection laws for any data it controls or processes.\n\n<a name=\"Delivery\"></a>\n## 17. Delivery\n\nThe Company will deliver the October CMS Software and this EULA to you by electronic download.\n\n<a name=\"General\"></a>\n## 18. General\n\n### 18.1 Amendments\n\nThe Company reserves the right to amend the terms and conditions of this EULA at any time and without giving any prior notice to you. You acknowledge that you are responsible for periodically reviewing this EULA to familiarise yourself with any changes. Your continued use of the October CMS Software after any changes to the EULA shall constitute your consent to such change. You can access the latest version of the EULA by visiting https://octobercms.com/eula\n\n### 18.2 Assignment\n\nYou may not assign any rights and obligations under this EULA, in whole or in part, without an authorised Company representative's written consent. Any attempt to assign any rights and obligations without the Company's consent shall be void. The Company reserves the right to assign any of its rights and obligations under this EULA to a third party without requiring your consent.\n\n### 18.3 Notices\n\nYou hereby consent to receive all notices and communication from the Company electronically.\n\nAll notices to the Company under this EULA shall be sent to:\n\nPO Box 47<br />\nQueanbeyan NSW 2620<br />\nAustralia\n\nFor any other questions relating to this EULA, please contact us at https://octobercms.com/contact\n\n### 18.4 Governing Law and Jurisdiction\n\nThis EULA shall be governed by and construed in accordance with the laws of the state of New South Wales, Australia, without giving effect to any principles of conflict of laws. The parties hereby agree to submit to the non-exclusive jurisdiction of the courts of New South Wales to decide any matter arising out of these Terms. Both Parties hereby agree that the United Nations Convention on Contracts for the International Sale of Goods will not apply to this EULA.\n\n### 18.5 Force Majeure\n\nNeither Party will be liable to the other for any failure or delay in the performance of its obligations to the extent that such failure or delay is caused by any unforeseen events which are beyond the reasonable control of the obligated Party such as an act of God, strike, war, terrorism, epidemic, internet or telecommunication outage or other similar events, and the obligated Party is not able to avoid or remove the force measure by taking reasonable measures.\n"
  },
  {
    "path": "README.md",
    "content": "October Rain\n=======\n\nThis repository contains the core library of October CMS. If you want to build a website using October, visit the main [October repository](http://github.com/octobercms/october).\n\n## License\n\nThe October CMS platform is licensed software, see [End User License Agreement](./LICENSE.md) (EULA) for more details.\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"october/rain\",\n    \"description\": \"October Rain Library\",\n    \"homepage\": \"http://octobercms.com\",\n    \"keywords\": [\"october\", \"cms\", \"rain\"],\n    \"authors\": [\n        {\n            \"name\": \"Alexey Bobkov\",\n            \"email\": \"aleksey.bobkov@gmail.com\"\n        },\n        {\n            \"name\": \"Samuel Georges\",\n            \"email\": \"daftspunky@gmail.com\"\n        }\n    ],\n    \"require\": {\n        \"php\": \"^8.1\",\n        \"composer/composer\": \"^2.0.0\",\n        \"composer/installers\": \"^1 || ^2\",\n        \"larajax/larajax\": \"^2.0\",\n        \"doctrine/dbal\": \"^2.13.3|^3.1.4\",\n        \"intervention/image\": \"^3.10\",\n        \"jaybizzle/crawler-detect\": \"^1.3\",\n        \"linkorb/jsmin-php\": \"~1.0\",\n        \"wikimedia/less.php\": \"~5.2\",\n        \"scssphp/scssphp\": \"~1.0\",\n        \"symfony/yaml\": \"^6.4|^7.0\",\n        \"twig/twig\": \"^3.21\",\n        \"league/csv\": \"~9.1\",\n        \"laravel/tinker\": \"~2.0|~3.0\",\n        \"symfony/html-sanitizer\": \"^6.1|^7.0\",\n        \"enshrined/svg-sanitize\": \"^0.22\"\n    },\n    \"require-dev\": {\n        \"laravel/framework\": \"^12.0\",\n        \"phpunit/phpunit\": \"^8.0|^9.0|^10.0|^11.0|^12.5.22\",\n        \"meyfa/phpunit-assert-gd\": \"^2.0.0|^3.0.0\",\n        \"phpbench/phpbench\": \"^1.4\"\n    },\n    \"autoload\": {\n        \"files\": [\n            \"init/init.php\"\n        ],\n        \"classmap\": [\n            \"globals/\"\n        ],\n        \"psr-4\": {\n            \"October\\\\Rain\\\\\": \"src/\",\n            \"October\\\\Contracts\\\\\": \"contracts/\"\n        }\n    },\n    \"autoload-dev\": {\n        \"classmap\": [\n            \"tests/TestCase.php\"\n        ]\n    },\n    \"scripts\": {\n        \"test\": [\n            \"phpunit --stop-on-failure\"\n        ],\n        \"bench\": [\n            \"phpbench run tests\\\\Benchmark\\\\ --report=default\"\n        ]\n    },\n    \"extra\": {\n        \"laravel\": {\n            \"providers\": [\n                \"October\\\\Rain\\\\Foundation\\\\Providers\\\\CoreServiceProvider\"\n            ]\n        }\n    },\n    \"minimum-stability\": \"dev\",\n    \"prefer-stable\": true,\n    \"config\": {\n        \"allow-plugins\": {\n            \"composer/installers\": true\n        }\n    }\n}\n"
  },
  {
    "path": "contracts/Database/CurrencyableInterface.php",
    "content": "<?php namespace October\\Contracts\\Database;\n\n/**\n * CurrencyableInterface\n *\n * @package october\\contracts\n * @author Alexey Bobkov, Samuel Georges\n */\ninterface CurrencyableInterface\n{\n    /**\n     * getCurrencyableAttributes returns the list of currencyable attribute names\n     * @return array\n     */\n    public function getCurrencyableAttributes();\n\n    /**\n     * isCurrencyableAttribute checks if a given attribute is currencyable\n     * @return bool\n     */\n    public function isCurrencyableAttribute($key);\n\n    /**\n     * isCurrencyableEnabled\n     * @return bool\n     */\n    public function isCurrencyableEnabled();\n}\n"
  },
  {
    "path": "contracts/Database/MultisiteGroupInterface.php",
    "content": "<?php namespace October\\Contracts\\Database;\n\n/**\n * MultisiteGroupInterface\n *\n * @package october\\contracts\n * @author Alexey Bobkov, Samuel Georges\n */\ninterface MultisiteGroupInterface\n{\n    /**\n     * isMultisiteGroupEnabled\n     * @return bool\n     */\n    public function isMultisiteGroupEnabled(): bool;\n\n    /**\n     * getSiteGroupIdColumn\n     * @return string\n     */\n    public function getSiteGroupIdColumn(): string;\n\n    /**\n     * getQualifiedSiteGroupIdColumn\n     * @return string\n     */\n    public function getQualifiedSiteGroupIdColumn(): string;\n}\n"
  },
  {
    "path": "contracts/Database/MultisiteInterface.php",
    "content": "<?php namespace October\\Contracts\\Database;\n\n/**\n * MultisiteInterface\n *\n * @package october\\contracts\n * @author Alexey Bobkov, Samuel Georges\n */\ninterface MultisiteInterface\n{\n    /**\n     * findOrCreateForSite\n     */\n    public function findOrCreateForSite(?string $siteId = null);\n\n    /**\n     * isMultisiteEnabled\n     * @return bool\n     */\n    public function isMultisiteEnabled();\n\n    /**\n     * isMultisiteSyncEnabled\n     * @return bool\n     */\n    public function isMultisiteSyncEnabled();\n}\n"
  },
  {
    "path": "contracts/Database/NestedSetInterface.php",
    "content": "<?php namespace October\\Contracts\\Database;\n\n/**\n * NestedSetInterface\n *\n * @package october\\contracts\n * @author Alexey Bobkov, Samuel Georges\n */\ninterface NestedSetInterface\n{\n    /**\n     * moveAfter\n     */\n    public function moveAfter($node);\n\n    /**\n     * moveBefore\n     */\n    public function moveBefore($node);\n\n    /**\n     * makeChildOf\n     */\n    public function makeChildOf($node);\n}\n"
  },
  {
    "path": "contracts/Database/SoftDeleteInterface.php",
    "content": "<?php namespace October\\Contracts\\Database;\n\n/**\n * SoftDeleteInterface\n *\n * @package october\\contracts\n * @author Alexey Bobkov, Samuel Georges\n */\ninterface SoftDeleteInterface\n{\n    /**\n     * trashed\n     * @return bool\n     */\n    public function trashed();\n\n    /**\n     * restore\n     * @return bool|null\n     */\n    public function restore();\n\n    /**\n     * forceDelete on a soft deleted model.\n     * @return void\n     */\n    public function forceDelete();\n}\n"
  },
  {
    "path": "contracts/Database/SortableInterface.php",
    "content": "<?php namespace October\\Contracts\\Database;\n\n/**\n * SortableInterface\n *\n * @package october\\contracts\n * @author Alexey Bobkov, Samuel Georges\n */\ninterface SortableInterface\n{\n    /**\n     * setSortableOrder\n     */\n    public function setSortableOrder($itemIds, $itemOrders = null);\n}\n"
  },
  {
    "path": "contracts/Database/SortableRelationInterface.php",
    "content": "<?php namespace October\\Contracts\\Database;\n\n/**\n * SortableRelationInterface\n *\n * @package october\\contracts\n * @author Alexey Bobkov, Samuel Georges\n */\ninterface SortableRelationInterface\n{\n    /**\n     * setSortableRelationOrder sets the sort order of records to the specified orders. If the orders is\n     * undefined, the record identifier is used.\n     * @param string $relationName\n     * @param mixed $itemIds\n     * @param array $itemOrders\n     */\n    public function setSortableRelationOrder($relationName, $itemIds, $itemOrders = null);\n\n    /**\n     * isSortableRelation returns true if the supplied relation is sortable.\n     */\n    public function isSortableRelation($relationName);\n}\n"
  },
  {
    "path": "contracts/Database/TranslatableInterface.php",
    "content": "<?php namespace October\\Contracts\\Database;\n\n/**\n * TranslatableInterface\n *\n * @package october\\contracts\n * @author Alexey Bobkov, Samuel Georges\n */\ninterface TranslatableInterface\n{\n    /**\n     * getTranslatableAttributes returns the list of translatable attribute names\n     * @return array\n     */\n    public function getTranslatableAttributes();\n\n    /**\n     * isTranslatableAttribute checks if a given attribute is translatable\n     * @return bool\n     */\n    public function isTranslatableAttribute($key);\n\n    /**\n     * isTranslatableEnabled\n     * @return bool\n     */\n    public function isTranslatableEnabled();\n}\n"
  },
  {
    "path": "contracts/Database/TreeInterface.php",
    "content": "<?php namespace October\\Contracts\\Database;\n\nuse Illuminate\\Support\\Collection;\n\n/**\n * TreeInterface\n *\n * @package october\\contracts\n * @author Alexey Bobkov, Samuel Georges\n */\ninterface TreeInterface\n{\n    /**\n     * getChildren\n     */\n    public function getChildren(): Collection;\n\n    /**\n     * getChildCount\n     */\n    public function getChildCount(): int;\n\n    /**\n     * scopeGetNested\n     */\n    public function scopeGetNested($query);\n\n    /**\n     * scopeListsNested\n     */\n    public function scopeListsNested($query, $column, $key = null, $indent = '&nbsp;&nbsp;&nbsp;');\n}\n"
  },
  {
    "path": "contracts/Database/ValidationInterface.php",
    "content": "<?php namespace October\\Contracts\\Database;\n\n/**\n * ValidationInterface\n *\n * @package october\\contracts\n * @author Alexey Bobkov, Samuel Georges\n */\ninterface ValidationInterface\n{\n    /**\n     * setValidationAttributeNames\n     */\n    public function setValidationAttributeNames($attributeNames);\n\n    /**\n     * isAttributeRequired\n     * @return bool\n     */\n    public function isAttributeRequired($attribute, $checkDependencies = true);\n}\n"
  },
  {
    "path": "contracts/Element/FilterElement.php",
    "content": "<?php namespace October\\Contracts\\Element;\n\nuse October\\Rain\\Element\\Filter\\ScopeDefinition;\n\n/**\n * FilterElement\n *\n * @package october\\contracts\n * @author Alexey Bobkov, Samuel Georges\n */\ninterface FilterElement\n{\n    /**\n     * defineScope adds a scope to the filter element\n     */\n    public function defineScope(string $scopeName, ?string $label = null): ScopeDefinition;\n}\n"
  },
  {
    "path": "contracts/Element/FormElement.php",
    "content": "<?php namespace October\\Contracts\\Element;\n\nuse October\\Rain\\Element\\Form\\FieldDefinition;\nuse October\\Rain\\Element\\Form\\FieldsetDefinition;\n\n/**\n * FormElement\n *\n * @package october\\contracts\n * @author Alexey Bobkov, Samuel Georges\n */\ninterface FormElement\n{\n    /**\n     * addFormField adds a field to the fieldset\n     */\n    public function addFormField(string $fieldName, ?string $label = null): FieldDefinition;\n\n    /**\n     * getFormFieldset returns the current fieldset definition\n     */\n    public function getFormFieldset(): FieldsetDefinition;\n\n    /**\n     * getFormContext returns the current form context, e.g. create, update\n     */\n    public function getFormContext();\n}\n"
  },
  {
    "path": "contracts/Element/ListElement.php",
    "content": "<?php namespace October\\Contracts\\Element;\n\nuse October\\Rain\\Element\\Lists\\ColumnDefinition;\n\n/**\n * ListElement\n *\n * @package october\\contracts\n * @author Alexey Bobkov, Samuel Georges\n */\ninterface ListElement\n{\n    /**\n     * defineColumn adds a column to the list element\n     */\n    public function defineColumn(string $columnName, ?string $label = null): ColumnDefinition;\n}\n"
  },
  {
    "path": "contracts/Support/OctoberPackage.php",
    "content": "<?php namespace October\\Contracts\\Support;\n\n/**\n * OctoberPackage\n *\n * @package october\\contracts\n * @author Alexey Bobkov, Samuel Georges\n */\ninterface OctoberPackage\n{\n    /**\n     * registerMarkupTags registers Twig markup tags introduced by this package.\n     *\n     *     return [\n     *         'filters' => [],\n     *         'functions' => []\n     *     ];\n     *\n     * @return array\n     */\n    public function registerMarkupTags();\n\n    /**\n     * registerComponents registers any CMS components implemented in this package.\n     *\n     *     return [\n     *        \\Acme\\Demo\\Components\\LocalePicker::class => 'localePicker',\n     *     ];\n     *\n     * @return array\n     */\n    public function registerComponents();\n\n    /**\n     * registerPageSnippets registers any CMS snippets implemented in this package.\n     *\n     *     return [\n     *        \\Acme\\Demo\\Components\\YouTubeVideo::class => 'youtubeVideo',\n     *     ];\n     *\n     * @return array\n     */\n    public function registerPageSnippets();\n\n    /**\n     * registerContentFields registers content fields used by tailor implemented in this package.\n     *\n     *     return [\n     *        \\Tailor\\ContentFields\\TextareaField::class => 'textarea',\n     *     ];\n     *\n     * @return array\n     */\n    public function registerContentFields();\n\n    /**\n     * registerNavigation registers backend navigation items for this package.\n     *\n     *     return [\n     *         'blog' => []\n     *     ];\n     *\n     * @return array\n     */\n    public function registerNavigation();\n\n    /**\n     * registerPermissions registers any permissions used by this package.\n     *\n     *     return [\n     *         'general.backend' => [\n     *             'label' => 'Access the Backend Panel',\n     *             'tab' => 'General'\n     *         ],\n     *     ];\n     *\n     * @return array\n     */\n    public function registerPermissions();\n\n    /**\n     * registerSettings registers any backend configuration links used by this package.\n     *\n     *     return [\n     *         'updates' => []\n     *     ];\n     *\n     * @return array\n     */\n    public function registerSettings();\n\n    /**\n     * registerReportWidgets registers any report widgets provided by this package.\n     * The widgets must be returned in the following format:\n     *\n     *     return [\n     *         'className1' => [\n     *             'label' => 'My widget 1',\n     *             'context' => ['context-1', 'context-2'],\n     *         ],\n     *         'className2' => [\n     *             'label' => 'My widget 2',\n     *             'context' => 'context-1'\n     *         ]\n     *     ];\n     *\n     * @return array\n     */\n    public function registerReportWidgets();\n\n    /**\n     * registerFormWidgets registers any form widgets implemented in this package.\n     * The widgets must be returned in the following format:\n     *\n     *     return [\n     *         ['className1' => 'alias'],\n     *         ['className2' => 'anotherAlias']\n     *     ];\n     *\n     * @return array\n     */\n    public function registerFormWidgets();\n\n    /**\n     * registerFilterWidgets registers any filter widgets implemented in this package.\n     * The widgets must be returned in the following format:\n     *\n     *     return [\n     *         ['className1' => 'alias'],\n     *         ['className2' => 'anotherAlias']\n     *     ];\n     *\n     * @return array\n     */\n    public function registerFilterWidgets();\n\n    /**\n     * registerListColumnTypes registers custom backend list column types introduced\n     * by this package.\n     *\n     * @return array\n     */\n    public function registerListColumnTypes();\n\n    /**\n     * registerMailLayouts registers any mail layouts implemented by this package.\n     * The layouts must be returned in the following format:\n     *\n     *     return [\n     *         'marketing' => 'acme.blog::layouts.marketing',\n     *         'notification' => 'acme.blog::layouts.notification',\n     *     ];\n     *\n     * @return array\n     */\n    public function registerMailLayouts();\n\n    /**\n     * registerMailTemplates registers any mail templates implemented by this package.\n     * The templates must be returned in the following format:\n     *\n     *     return [\n     *         'acme.blog::mail.welcome',\n     *         'acme.blog::mail.forgot_password',\n     *     ];\n     *\n     * @return array\n     */\n    public function registerMailTemplates();\n\n    /**\n     * registerMailPartials registers any mail partials implemented by this package.\n     * The partials must be returned in the following format:\n     *\n     *     return [\n     *         'tracking' => 'acme.blog::partials.tracking',\n     *         'promotion' => 'acme.blog::partials.promotion',\n     *     ];\n     *\n     * @return array\n     */\n    public function registerMailPartials();\n\n    /**\n     * registerSchedule registers scheduled tasks that are executed on a regular basis.\n     *\n     * @param \\Illuminate\\Console\\Scheduling\\Schedule $schedule\n     * @return void\n     */\n    public function registerSchedule($schedule);\n}\n"
  },
  {
    "path": "contracts/Twig/CallsAnyMethod.php",
    "content": "<?php namespace October\\Contracts\\Twig;\n\n/**\n * CallsAnyMethod from Twig engine\n *\n * @package october\\contracts\n * @author Alexey Bobkov, Samuel Georges\n */\ninterface CallsAnyMethod\n{\n}\n"
  },
  {
    "path": "contracts/Twig/CallsMethods.php",
    "content": "<?php namespace October\\Contracts\\Twig;\n\n/**\n * CallsMethods from Twig engine\n *\n * @package october\\contracts\n * @author Alexey Bobkov, Samuel Georges\n */\ninterface CallsMethods\n{\n    /**\n     * getTwigMethodNames returns a list of method names that can be called from Twig.\n     */\n    public function getTwigMethodNames(): array;\n}\n"
  },
  {
    "path": "globals/AjaxException.php",
    "content": "<?php\n\n/**\n * AjaxException\n *\n * @see October\\Rain\\Exception\\AjaxException\n */\nclass AjaxException extends October\\Rain\\Exception\\AjaxException {}\n"
  },
  {
    "path": "globals/App.php",
    "content": "<?php\n\n/**\n * App\n *\n * @see \\Illuminate\\Contracts\\Foundation\\Application\n */\nclass App extends Illuminate\\Support\\Facades\\App {}\n"
  },
  {
    "path": "globals/ApplicationException.php",
    "content": "<?php\n\n/**\n * ApplicationException\n *\n * @see October\\Rain\\Exception\\ApplicationException\n */\nclass ApplicationException extends October\\Rain\\Exception\\ApplicationException {}\n"
  },
  {
    "path": "globals/Arr.php",
    "content": "<?php\n\n/**\n * Arr\n *\n * @see \\October\\Rain\\Support\\Arr\n */\nclass Arr extends October\\Rain\\Support\\Arr {}\n"
  },
  {
    "path": "globals/Artisan.php",
    "content": "<?php\n\n/**\n * Artisan\n *\n * @see \\Illuminate\\Contracts\\Console\\Kernel\n */\nclass Artisan extends Illuminate\\Support\\Facades\\Artisan {}\n"
  },
  {
    "path": "globals/Auth.php",
    "content": "<?php\n\n/**\n * Auth\n *\n * @see \\RainLab\\User\\Classes\\AuthManager\n */\nclass Auth extends October\\Rain\\Support\\Facades\\Auth {}\n"
  },
  {
    "path": "globals/Backend.php",
    "content": "<?php\n\n/**\n * Backend\n *\n * @see \\Backend\\Helpers\\Backend\n */\nclass Backend extends Backend\\Facades\\Backend {}\n"
  },
  {
    "path": "globals/BackendAuth.php",
    "content": "<?php\n\n/**\n * BackendAuth\n *\n * @see \\Backend\\Classes\\AuthManager\n */\nclass BackendAuth extends Backend\\Facades\\BackendAuth {}\n"
  },
  {
    "path": "globals/BackendMenu.php",
    "content": "<?php\n\n/**\n * BackendMenu\n *\n * @see \\Backend\\Classes\\NavigationManager\n */\nclass BackendMenu extends Backend\\Facades\\BackendMenu {}\n"
  },
  {
    "path": "globals/BackendUi.php",
    "content": "<?php\n\n/**\n * @deprecated see \\Ui\n */\nclass BackendUi extends Backend\\Facades\\BackendUi {}\n"
  },
  {
    "path": "globals/Block.php",
    "content": "<?php\n\n/**\n * Block\n *\n * @see \\October\\Rain\\Html\\BlockBuilder\n */\nclass Block extends October\\Rain\\Support\\Facades\\Block {}\n"
  },
  {
    "path": "globals/Broadcast.php",
    "content": "<?php\n\n/**\n * Broadcast\n *\n * @see \\Illuminate\\Contracts\\Broadcasting\\Factory\n */\nclass Broadcast extends Illuminate\\Support\\Facades\\Broadcast {}\n"
  },
  {
    "path": "globals/Bus.php",
    "content": "<?php\n\n/**\n * Bus\n *\n * @see \\Illuminate\\Contracts\\Bus\\Dispatcher\n */\nclass Bus extends Illuminate\\Support\\Facades\\Bus {}\n"
  },
  {
    "path": "globals/Cache.php",
    "content": "<?php\n\n/**\n * Cache\n *\n * @see \\Illuminate\\Cache\\CacheManager\n * @see \\Illuminate\\Cache\\Repository\n */\nclass Cache extends Illuminate\\Support\\Facades\\Cache {}\n"
  },
  {
    "path": "globals/Cms.php",
    "content": "<?php\n\n/**\n * Cms\n *\n * @see \\Cms\\Helpers\\Cms\n */\nclass Cms extends Cms\\Facades\\Cms {}\n"
  },
  {
    "path": "globals/Config.php",
    "content": "<?php\n\n/**\n * Config\n *\n * @see \\Illuminate\\Config\\Repository\n */\nclass Config extends Illuminate\\Support\\Facades\\Config {}\n"
  },
  {
    "path": "globals/Cookie.php",
    "content": "<?php\n\n/**\n * Cookie\n *\n * @see \\Illuminate\\Cookie\\CookieJar\n */\nclass Cookie extends Illuminate\\Support\\Facades\\Cookie {}\n"
  },
  {
    "path": "globals/Crypt.php",
    "content": "<?php\n\n/**\n * Crypt\n *\n * @see \\Illuminate\\Encryption\\Encrypter\n */\nclass Crypt extends Illuminate\\Support\\Facades\\Crypt {}\n"
  },
  {
    "path": "globals/Currency.php",
    "content": "<?php\n\n/**\n * Currency\n *\n * @see \\Responsiv\\Currency\\Classes\\CurrencyManager\n */\nclass Currency extends October\\Rain\\Support\\Facades\\Currency {}\n"
  },
  {
    "path": "globals/Date.php",
    "content": "<?php\n\n/**\n * Date\n *\n * @see Illuminate\\Support\\DateFactory\n */\nclass Date extends Illuminate\\Support\\Facades\\Date {}\n"
  },
  {
    "path": "globals/Db.php",
    "content": "<?php\n\n/**\n * Db\n *\n * @see \\Illuminate\\Database\\DatabaseManager\n * @see \\Illuminate\\Database\\Connection\n */\nclass Db extends Illuminate\\Support\\Facades\\DB {}\n"
  },
  {
    "path": "globals/DbDongle.php",
    "content": "<?php\n\n/**\n * DbDongle\n *\n * @see \\October\\Rain\\Database\\Dongle\n */\nclass DbDongle extends October\\Rain\\Support\\Facades\\DbDongle {}\n"
  },
  {
    "path": "globals/Event.php",
    "content": "<?php\n\n/**\n * Event\n *\n * @see \\October\\Rain\\Events\\Dispatcher\n */\nclass Event extends October\\Rain\\Support\\Facades\\Event {}\n"
  },
  {
    "path": "globals/File.php",
    "content": "<?php\n\n/**\n * File\n *\n * @see \\October\\Rain\\Filesystem\\Filesystem\n */\nclass File extends October\\Rain\\Support\\Facades\\File {}\n"
  },
  {
    "path": "globals/Flash.php",
    "content": "<?php\n\n/**\n * Flash\n *\n * @see \\October\\Rain\\Flash\\FlashBag\n */\nclass Flash extends October\\Rain\\Support\\Facades\\Flash {}\n"
  },
  {
    "path": "globals/ForbiddenException.php",
    "content": "<?php\n\n/**\n * ForbiddenException\n *\n * @see October\\Rain\\Exception\\ForbiddenException\n */\nclass ForbiddenException extends October\\Rain\\Exception\\ForbiddenException {}\n"
  },
  {
    "path": "globals/Form.php",
    "content": "<?php\n\n/**\n * Form\n *\n * @see \\October\\Rain\\Html\\FormBuilder\n */\nclass Form extends October\\Rain\\Support\\Facades\\Form {}\n"
  },
  {
    "path": "globals/Hash.php",
    "content": "<?php\n\n/**\n * Hash\n *\n * @see \\Illuminate\\Hashing\\HashManager\n */\nclass Hash extends Illuminate\\Support\\Facades\\Hash {}\n"
  },
  {
    "path": "globals/Html.php",
    "content": "<?php\n\n/**\n * Html\n *\n * @see \\October\\Rain\\Html\\HtmlBuilder\n */\nclass Html extends October\\Rain\\Support\\Facades\\Html {}\n"
  },
  {
    "path": "globals/Http.php",
    "content": "<?php\n\n/**\n * Http\n *\n * @see \\Illuminate\\Http\\Client\\Factory\n */\nclass Http extends Illuminate\\Support\\Facades\\Http {}\n"
  },
  {
    "path": "globals/Ini.php",
    "content": "<?php\n\n/**\n * Ini\n *\n * @see \\October\\Rain\\Parse\\Ini\n */\nclass Ini extends October\\Rain\\Support\\Facades\\Ini {}\n"
  },
  {
    "path": "globals/Input.php",
    "content": "<?php\n\n/**\n * Input\n *\n * @see Illuminate\\Http\\Request\n */\nclass Input extends October\\Rain\\Support\\Facades\\Input {}\n"
  },
  {
    "path": "globals/Lang.php",
    "content": "<?php\n\n/**\n * Lang\n *\n * @see \\Illuminate\\Translation\\Translator\n */\nclass Lang extends Illuminate\\Support\\Facades\\Lang {}\n"
  },
  {
    "path": "globals/Log.php",
    "content": "<?php\n\n/**\n * Log\n *\n * @see \\Illuminate\\Log\\Logger\n */\nclass Log extends Illuminate\\Support\\Facades\\Log {}\n"
  },
  {
    "path": "globals/Mail.php",
    "content": "<?php\n\n/**\n * Mail\n *\n * @see \\October\\Rain\\Mail\\Mailer\n */\nclass Mail extends October\\Rain\\Support\\Facades\\Mail {}\n"
  },
  {
    "path": "globals/Manifest.php",
    "content": "<?php\n\n/**\n * Manifest\n *\n * @see \\System\\Classes\\ManifestCache\n */\nclass Manifest extends System\\Facades\\Manifest {}\n"
  },
  {
    "path": "globals/Markdown.php",
    "content": "<?php\n\n/**\n * Markdown\n *\n * @see \\October\\Rain\\Parse\\Markdown\n */\nclass Markdown extends October\\Rain\\Support\\Facades\\Markdown {}\n"
  },
  {
    "path": "globals/Model.php",
    "content": "<?php\n\n/**\n * Model\n *\n * @see October\\Rain\\Database\\Model\n */\nclass Model extends October\\Rain\\Database\\Model {}\n"
  },
  {
    "path": "globals/NotFoundException.php",
    "content": "<?php\n\n/**\n * NotFoundException\n *\n * @see October\\Rain\\Exception\\NotFoundException\n */\nclass NotFoundException extends October\\Rain\\Exception\\NotFoundException {}\n"
  },
  {
    "path": "globals/Notification.php",
    "content": "<?php\n\n/**\n * Notification\n *\n * @see \\Illuminate\\Notifications\\ChannelManager\n */\nclass Notification extends Illuminate\\Support\\Facades\\Notification {}\n"
  },
  {
    "path": "globals/Password.php",
    "content": "<?php\n\n/**\n * Password\n *\n * @see \\Illuminate\\Auth\\Passwords\\PasswordBroker\n */\nclass Password extends Illuminate\\Support\\Facades\\Password {}\n"
  },
  {
    "path": "globals/Queue.php",
    "content": "<?php\n\n/**\n * Queue\n *\n * @see \\Illuminate\\Queue\\QueueManager\n */\nclass Queue extends Illuminate\\Support\\Facades\\Queue {}\n"
  },
  {
    "path": "globals/Redirect.php",
    "content": "<?php\n\n/**\n * Redirect\n *\n * @see \\Illuminate\\Routing\\Redirector\n */\nclass Redirect extends Illuminate\\Support\\Facades\\Redirect {}\n"
  },
  {
    "path": "globals/Redis.php",
    "content": "<?php\n\n/**\n * Redis\n *\n * @see \\Illuminate\\Redis\\RedisManager\n */\nclass Redis extends Illuminate\\Support\\Facades\\Redis {}\n"
  },
  {
    "path": "globals/Request.php",
    "content": "<?php\n\n/**\n * Request\n *\n * @see \\Illuminate\\Http\\Request\n */\nclass Request extends Illuminate\\Support\\Facades\\Request {}\n"
  },
  {
    "path": "globals/Resizer.php",
    "content": "<?php\n\n/**\n * Resizer\n *\n * @see \\October\\Rain\\Resize\\Resizer\n */\nclass Resizer extends October\\Rain\\Support\\Facades\\Resizer {}\n"
  },
  {
    "path": "globals/Response.php",
    "content": "<?php\n\n/**\n * Response\n *\n * @see \\Illuminate\\Contracts\\Routing\\ResponseFactory\n */\nclass Response extends Illuminate\\Support\\Facades\\Response {}\n"
  },
  {
    "path": "globals/Route.php",
    "content": "<?php\n\n/**\n * Route\n *\n * @see \\Illuminate\\Routing\\Router\n */\nclass Route extends Illuminate\\Support\\Facades\\Route {}\n"
  },
  {
    "path": "globals/Schema.php",
    "content": "<?php\n\n/**\n * Schema\n *\n * @see \\Illuminate\\Database\\Schema\\Builder\n */\nclass Schema extends October\\Rain\\Support\\Facades\\Schema {}\n"
  },
  {
    "path": "globals/Seeder.php",
    "content": "<?php\n\n/**\n * Seeder\n *\n * @see October\\Rain\\Database\\Updates\\Seeder\n */\nclass Seeder extends October\\Rain\\Database\\Updates\\Seeder {}\n"
  },
  {
    "path": "globals/Session.php",
    "content": "<?php\n\n/**\n * Session\n *\n * @see \\Illuminate\\Session\\Store\n */\nclass Session extends Illuminate\\Support\\Facades\\Session {}\n"
  },
  {
    "path": "globals/Site.php",
    "content": "<?php\n\n/**\n * Site\n *\n * @see \\System\\Classes\\SiteManager\n */\nclass Site extends October\\Rain\\Support\\Facades\\Site {}\n"
  },
  {
    "path": "globals/Storage.php",
    "content": "<?php\n\n/**\n * Storage\n *\n * @see \\Illuminate\\Filesystem\\FilesystemManager\n */\nclass Storage extends Illuminate\\Support\\Facades\\Storage {}\n"
  },
  {
    "path": "globals/Str.php",
    "content": "<?php\n\n/**\n * Str\n *\n * @see Illuminate\\Support\\Str\n */\nclass Str extends October\\Rain\\Support\\Str {}\n"
  },
  {
    "path": "globals/System.php",
    "content": "<?php\n\n/**\n * System\n *\n * @see \\System\\Helpers\\System\n */\nclass System extends System\\Facades\\System {}\n"
  },
  {
    "path": "globals/SystemException.php",
    "content": "<?php\n\n/**\n * SystemException\n *\n * @see October\\Rain\\Exception\\SystemException\n */\nclass SystemException extends October\\Rain\\Exception\\SystemException {}\n"
  },
  {
    "path": "globals/Twig.php",
    "content": "<?php\n\n/**\n * Twig\n *\n * @see \\October\\Rain\\Parse\\Twig\n */\nclass Twig extends October\\Rain\\Support\\Facades\\Twig {}\n"
  },
  {
    "path": "globals/Ui.php",
    "content": "<?php\n\n/**\n * Ui\n *\n * @see \\System\\Classes\\UiManager\n */\nclass Ui extends System\\Facades\\Ui {}\n"
  },
  {
    "path": "globals/Url.php",
    "content": "<?php\n\n/**\n * Url\n *\n * @see \\Illuminate\\Routing\\UrlGenerator\n */\nclass Url extends October\\Rain\\Support\\Facades\\Url {}\n"
  },
  {
    "path": "globals/ValidationException.php",
    "content": "<?php\n\n/**\n * ValidationException\n *\n * @see October\\Rain\\Exception\\ValidationException\n */\nclass ValidationException extends October\\Rain\\Exception\\ValidationException {}\n"
  },
  {
    "path": "globals/Validator.php",
    "content": "<?php\n\n/**\n * Validator\n *\n * @see \\October\\Rain\\Validation\\Factory\n */\nclass Validator extends October\\Rain\\Support\\Facades\\Validator {}\n"
  },
  {
    "path": "globals/View.php",
    "content": "<?php\n\n/**\n * View\n *\n * @see \\Illuminate\\View\\Factory\n */\nclass View extends Illuminate\\Support\\Facades\\View {}\n"
  },
  {
    "path": "globals/Vite.php",
    "content": "<?php\n\n/**\n * Vite\n *\n * @see \\Illuminate\\Foundation\\Vite\n */\nclass Vite extends Illuminate\\Support\\Facades\\Vite {}\n"
  },
  {
    "path": "globals/Yaml.php",
    "content": "<?php\n\n/**\n * Yaml\n *\n * @see \\October\\Rain\\Parse\\Yaml\n */\nclass Yaml extends October\\Rain\\Support\\Facades\\Yaml {}\n"
  },
  {
    "path": "init/autoloader.php",
    "content": "<?php\n\nuse October\\Rain\\Composer\\ClassLoader;\n\nClassLoader::configure(dirname(__DIR__, 4))\n    ->withNamespace('App\\\\', 'app')\n    ->withDirectories([\n        'modules',\n        'plugins'\n    ])\n    ->register();\n"
  },
  {
    "path": "init/functions.php",
    "content": "<?php\n\nuse October\\Rain\\Support\\Arr;\nuse October\\Rain\\Support\\Str;\nuse October\\Rain\\Support\\Collection;\n\nif (!function_exists('input')) {\n    /**\n     * input returns all user input or the default value ($_POST + $_GET + $_FILES).\n     * Supports HTML Array names.\n     *\n     *     $value = input('value', 'not found');\n     *     $name = input('contact[name]');\n     *     $name = input('contact[location][city]');\n     *\n     * Booleans are converted from string values.\n     * @param string $name\n     * @param string $default\n     * @return mixed\n     */\n    function input($name = null, $default = null)\n    {\n        if ($name === null) {\n            return Request::all();\n        }\n\n        // Array field name, eg: field[key][key2][key3]\n        if (class_exists(\\October\\Rain\\Html\\Helper::class)) {\n            $name = October\\Rain\\Html\\Helper::nameToDot($name);\n        }\n\n        return array_get(Request::all(), $name, $default);\n    }\n}\n\nif (!function_exists('post')) {\n    /**\n     * post is an identical function to input(), however restricted to POST methods ($_POST).\n     */\n    function post($name = null, $default = null)\n    {\n        if (Request::getRealMethod() !== 'POST') {\n            return $default;\n        }\n\n        if ($name === null) {\n            return Request::post();\n        }\n\n        // Array field name, eg: field[key][key2][key3]\n        if (class_exists(\\October\\Rain\\Html\\Helper::class)) {\n            $name = October\\Rain\\Html\\Helper::nameToDot($name);\n        }\n\n        return array_get(Request::post(), $name, $default);\n    }\n}\n\nif (!function_exists('get')) {\n    /**\n     * get is an identical function to input(), however restricted to GET values ($_GET).\n     */\n    function get($name = null, $default = null)\n    {\n        if ($name === null) {\n            return Request::query();\n        }\n\n        // Array field name, eg: field[key][key2][key3]\n        if (class_exists(\\October\\Rain\\Html\\Helper::class)) {\n            $name = October\\Rain\\Html\\Helper::nameToDot($name);\n        }\n\n        return array_get(Request::query(), $name, $default);\n    }\n}\n\nif (!function_exists('files')) {\n    /**\n     * files obtains a file item from the request ($_FILES).\n     */\n    function files($name = null, $default = null)\n    {\n        if ($name === null) {\n            return Request::allFiles();\n        }\n\n        // Array field name, eg: field[key][key2][key3]\n        if (class_exists(\\October\\Rain\\Html\\Helper::class)) {\n            $name = October\\Rain\\Html\\Helper::nameToDot($name);\n        }\n\n        return array_get(Request::allFiles(), $name, $default);\n    }\n}\n\nif (!function_exists('trace_log')) {\n    /**\n     * trace_log writes a trace message to a log file\n     */\n    function trace_log()\n    {\n        $messages = func_get_args();\n\n        foreach ($messages as $message) {\n            $level = 'info';\n\n            if ($message instanceof Exception) {\n                $level = 'error';\n            }\n            elseif (is_array($message) || is_object($message)) {\n                $message = print_r($message, true);\n            }\n\n            Log::$level($message);\n        }\n    }\n}\n\nif (!function_exists('traceLog')) {\n    /**\n     * traceLog is an alias for trace_log()\n     */\n    function traceLog()\n    {\n        call_user_func_array('trace_log', func_get_args());\n    }\n}\n\nif (!function_exists('trace_sql')) {\n    /**\n     * trace_sql begins to monitor all SQL output\n     * @return void\n     */\n    function trace_sql()\n    {\n        if (!defined('OCTOBER_NO_EVENT_LOGGING')) {\n            define('OCTOBER_NO_EVENT_LOGGING', 1);\n        }\n\n        if (!defined('OCTOBER_TRACING_SQL')) {\n            define('OCTOBER_TRACING_SQL', 1);\n        }\n        else {\n            return;\n        }\n\n        Event::listen('illuminate.query', function ($query, $bindings, $time, $name) {\n            $data = compact('bindings', 'time', 'name');\n\n            foreach ($bindings as $i => $binding) {\n                if ($binding instanceof \\DateTime) {\n                    $bindings[$i] = $binding->format('\\'Y-m-d H:i:s\\'');\n                }\n                elseif (is_string($binding)) {\n                    $bindings[$i] = \"'$binding'\";\n                }\n            }\n\n            $query = str_replace(['%', '?'], ['%%', '%s'], $query);\n            $query = vsprintf($query, $bindings);\n\n            Log::info($query);\n        });\n    }\n}\n\nif (!function_exists('traceSql')) {\n    /**\n     * traceSql is an alias for trace_sql()\n     * @return void\n     */\n    function traceSql()\n    {\n        trace_sql();\n    }\n}\n\nif (!function_exists('traceBack')) {\n    /**\n     * traceBack is an alias for trace_back()\n     */\n    function traceBack(int $distance = 25)\n    {\n        trace_back($distance);\n    }\n}\n\nif (!function_exists('trace_back')) {\n    /**\n     * trace_back logs a simple backtrace from the call point\n     * @return void\n     */\n    function trace_back(int $distance = 25)\n    {\n        trace_log(debug_backtrace(2, $distance));\n    }\n}\n\nif (!function_exists('plugins_path')) {\n    /**\n     * plugins_path gets the path to the plugins folder\n     * @param  string  $path\n     * @return string\n     */\n    function plugins_path($path = '')\n    {\n        return app('path.plugins').($path ? '/'.$path : $path);\n    }\n}\n\nif (!function_exists('cache_path')) {\n    /**\n     * cache_path gets the path to the cache folder\n     * @param  string  $path\n     * @return string\n     */\n    function cache_path($path = '')\n    {\n        return app('path.cache').($path ? '/'.$path : $path);\n    }\n}\n\nif (!function_exists('themes_path')) {\n    /**\n     * themes_path gets the path to the themes folder\n     * @param  string  $path\n     * @return string\n     */\n    function themes_path($path = '')\n    {\n        return app('path.themes').($path ? '/'.$path : $path);\n    }\n}\n\nif (!function_exists('temp_path')) {\n    /**\n     * temp_path gets the path to the temporary storage folder\n     * @param  string  $path\n     * @return string\n     */\n    function temp_path($path = '')\n    {\n        return app('path.temp').($path ? '/'.$path : $path);\n    }\n}\n\nif (!function_exists('e')) {\n    /**\n     * e will encode HTML special characters in a string\n     * @param  \\Illuminate\\Contracts\\Support\\Htmlable|string  $value\n     * @param  bool  $doubleEncode\n     * @return string\n     */\n    function e($value, $doubleEncode = false)\n    {\n        if ($value instanceof \\Illuminate\\Contracts\\Support\\Htmlable) {\n            return $value->toHtml();\n        }\n\n        return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8', $doubleEncode);\n    }\n}\n\nif (!function_exists('trans')) {\n    /**\n     * trans translates the given message\n     * @param  string  $id\n     * @param  array   $parameters\n     * @param  string  $locale\n     * @return string\n     */\n    function trans($id = null, $parameters = [], $locale = null)\n    {\n        return app('translator')->trans($id, $parameters, $locale);\n    }\n}\n\nif (!function_exists('array_build')) {\n    /**\n     * array_build builds a new array using a callback\n     * @param  array  $array\n     * @param  callable  $callback\n     * @return array\n     */\n    function array_build($array, callable $callback)\n    {\n        return Arr::build($array, $callback);\n    }\n}\n\nif (!function_exists('collect')) {\n    /**\n     * collect creates a collection from the given value\n     * @param  mixed  $value\n     * @return \\October\\Rain\\Support\\Collection\n     */\n    function collect($value = null)\n    {\n        return new Collection($value);\n    }\n}\n\nif (!function_exists('array_add')) {\n    /**\n     * array_add adds an element to an array using \"dot\" notation if it doesn't exist\n     * @param  array  $array\n     * @param  string  $key\n     * @param  mixed  $value\n     * @return array\n     */\n    function array_add($array, $key, $value)\n    {\n        return Arr::add($array, $key, $value);\n    }\n}\n\nif (!function_exists('array_collapse')) {\n    /**\n     * array_collapse collapses an array of arrays into a single array\n     * @param  array  $array\n     * @return array\n     */\n    function array_collapse($array)\n    {\n        return Arr::collapse($array);\n    }\n}\n\nif (!function_exists('array_divide')) {\n    /**\n     * array_divide divides an array into two arrays. One with keys and the other with values\n     * @param  array  $array\n     * @return array\n     */\n    function array_divide($array)\n    {\n        return Arr::divide($array);\n    }\n}\n\nif (!function_exists('array_dot')) {\n    /**\n     * array_dot flattens a multi-dimensional associative array with dots\n     * @param  array  $array\n     * @param  string  $prepend\n     * @return array\n     */\n    function array_dot($array, $prepend = '')\n    {\n        return Arr::dot($array, $prepend);\n    }\n}\n\nif (!function_exists('array_except')) {\n    /**\n     * array_except gets all of the given array except for a specified array of keys\n     * @param  array  $array\n     * @param  array|string  $keys\n     * @return array\n     */\n    function array_except($array, $keys)\n    {\n        return Arr::except($array, $keys);\n    }\n}\n\nif (!function_exists('array_flatten')) {\n    /**\n     * array_flatten flattens a multi-dimensional array into a single level\n     * @param  array  $array\n     * @param  int  $depth\n     * @return array\n     */\n    function array_flatten($array, $depth = PHP_INT_MAX)\n    {\n        return Arr::flatten($array, $depth);\n    }\n}\n\nif (!function_exists('array_forget')) {\n    /**\n     * array_forget removes one or many array items from a given array using \"dot\" notation\n     * @param  array  $array\n     * @param  array|string  $keys\n     * @return void\n     */\n    function array_forget(&$array, $keys)\n    {\n        Arr::forget($array, $keys);\n    }\n}\n\nif (!function_exists('array_get')) {\n    /**\n     * array_get gets an item from an array using \"dot\" notation\n     * @param  \\ArrayAccess|array  $array\n     * @param  string|int  $key\n     * @param  mixed  $default\n     * @return mixed\n     */\n    function array_get($array, $key, $default = null)\n    {\n        return Arr::get($array, $key, $default);\n    }\n}\n\nif (!function_exists('array_has')) {\n    /**\n     * array_has checks if an item or items exist in an array using \"dot\" notation\n     * @param  \\ArrayAccess|array  $array\n     * @param  string|array  $keys\n     * @return bool\n     */\n    function array_has($array, $keys)\n    {\n        return Arr::has($array, $keys);\n    }\n}\n\nif (!function_exists('array_only')) {\n    /**\n     * array_only gets a subset of the items from the given array\n     * @param  array  $array\n     * @param  array|string  $keys\n     * @return array\n     */\n    function array_only($array, $keys)\n    {\n        return Arr::only($array, $keys);\n    }\n}\n\nif (!function_exists('array_pluck')) {\n    /**\n     * array_pluck plucks an array of values from an array\n     * @param  array  $array\n     * @param  string|array  $value\n     * @param  string|array|null  $key\n     * @return array\n     */\n    function array_pluck($array, $value, $key = null)\n    {\n        return Arr::pluck($array, $value, $key);\n    }\n}\n\nif (!function_exists('array_prepend')) {\n    /**\n     * array_prepend pushes an item onto the beginning of an array\n     * @param  array  $array\n     * @param  mixed  $value\n     * @param  mixed  $key\n     * @return array\n     */\n    function array_prepend($array, $value, $key = null)\n    {\n        return Arr::prepend($array, $value, $key);\n    }\n}\n\nif (!function_exists('array_pull')) {\n    /**\n     * array_pull gets a value from the array, and remove it\n     * @param  array  $array\n     * @param  string  $key\n     * @param  mixed  $default\n     * @return mixed\n     */\n    function array_pull(&$array, $key, $default = null)\n    {\n        return Arr::pull($array, $key, $default);\n    }\n}\n\nif (!function_exists('array_random')) {\n    /**\n     * array_random gets a random value from an array\n     * @param  array  $array\n     * @param  int|null  $num\n     * @return mixed\n     */\n    function array_random($array, $num = null)\n    {\n        return Arr::random($array, $num);\n    }\n}\n\nif (!function_exists('array_set')) {\n    /**\n     * array_set sets an array item to a given value using \"dot\" notation\n     * If no key is given to the method, the entire array will be replaced.\n     * @param  array  $array\n     * @param  string  $key\n     * @param  mixed  $value\n     * @return array\n     */\n    function array_set(&$array, $key, $value)\n    {\n        return Arr::set($array, $key, $value);\n    }\n}\n\nif (!function_exists('array_sort')) {\n    /**\n     * array_sort sorts the array by the given callback or attribute name\n     * @param  array  $array\n     * @param  callable|string|null  $callback\n     * @return array\n     */\n    function array_sort($array, $callback = null)\n    {\n        return Arr::sort($array, $callback);\n    }\n}\n\nif (!function_exists('array_sort_recursive')) {\n    /**\n     * array_sort_recursive recursively sort an array by keys and values\n     * @param  array  $array\n     * @return array\n     */\n    function array_sort_recursive($array)\n    {\n        return Arr::sortRecursive($array);\n    }\n}\n\nif (!function_exists('array_where')) {\n    /**\n     * array_where filters the array using the given callback\n     * @param  array  $array\n     * @param  callable  $callback\n     * @return array\n     */\n    function array_where($array, callable $callback)\n    {\n        return Arr::where($array, $callback);\n    }\n}\n\nif (!function_exists('array_wrap')) {\n    /**\n     * array_wrap if the given value is not an array, wrap it in one\n     * @param  mixed  $value\n     * @return array\n     */\n    function array_wrap($value)\n    {\n        return Arr::wrap($value);\n    }\n}\n\nif (!function_exists('camel_case')) {\n    /**\n     * camel_case converts a value to camel case\n     * @param  string  $value\n     * @return string\n     */\n    function camel_case($value)\n    {\n        return Str::camel($value);\n    }\n}\n\nif (!function_exists('kebab_case')) {\n    /**\n     * kebab_case converts a string to kebab case\n     * @param  string  $value\n     * @return string\n     */\n    function kebab_case($value)\n    {\n        return Str::kebab($value);\n    }\n}\n\nif (!function_exists('snake_case')) {\n    /**\n     * snake_case converts a string to snake case\n     * @param  string  $value\n     * @param  string  $delimiter\n     * @return string\n     */\n    function snake_case($value, $delimiter = '_')\n    {\n        return Str::snake($value, $delimiter);\n    }\n}\n\nif (!function_exists('str_after')) {\n    /**\n     * str_after returns the remainder of a string after a given value\n     * @param  string  $subject\n     * @param  string  $search\n     * @return string\n     */\n    function str_after($subject, $search)\n    {\n        return Str::after($subject, $search);\n    }\n}\n\nif (!function_exists('str_before')) {\n    /**\n     * str_before get the portion of a string before a given value\n     * @param  string  $subject\n     * @param  string  $search\n     * @return string\n     */\n    function str_before($subject, $search)\n    {\n        return Str::before($subject, $search);\n    }\n}\n\nif (!function_exists('str_finish')) {\n    /**\n     * str_finish caps a string with a single instance of a given value\n     * @param  string  $value\n     * @param  string  $cap\n     * @return string\n     */\n    function str_finish($value, $cap)\n    {\n        return Str::finish($value, $cap);\n    }\n}\n\nif (!function_exists('str_is')) {\n    /**\n     * str_is determines if a given string matches a given pattern\n     * @param  string|array  $pattern\n     * @param  string  $value\n     * @return bool\n     */\n    function str_is($pattern, $value)\n    {\n        return Str::is($pattern, $value);\n    }\n}\n\nif (!function_exists('str_limit')) {\n    /**\n     * str_limit limits the number of characters in a string\n     * @param  string  $value\n     * @param  int  $limit\n     * @param  string  $end\n     * @return string\n     */\n    function str_limit($value, $limit = 100, $end = '...')\n    {\n        return Str::limit($value, $limit, $end);\n    }\n}\n\nif (!function_exists('str_plural')) {\n    /**\n     * str_plural get the plural form of an English word\n     * @param  string  $value\n     * @param  int  $count\n     * @return string\n     */\n    function str_plural($value, $count = 2)\n    {\n        return Str::plural($value, $count);\n    }\n}\n\nif (!function_exists('str_random')) {\n    /**\n     * str_random generates a more truly \"random\" alpha-numeric string\n     * @param  int  $length\n     * @return string\n     *\n     * @throws \\RuntimeException\n     */\n    function str_random($length = 16)\n    {\n        return Str::random($length);\n    }\n}\n\nif (!function_exists('str_replace_array')) {\n    /**\n     * str_replace_array replaces a given value in the string sequentially with an array\n     * @param  string  $search\n     * @param  array  $replace\n     * @param  string  $subject\n     * @return string\n     */\n    function str_replace_array($search, array $replace, $subject)\n    {\n        return Str::replaceArray($search, $replace, $subject);\n    }\n}\n\nif (!function_exists('str_replace_first')) {\n    /**\n     * str_replace_first replaces the first occurrence of a given value in the string\n     * @param  string  $search\n     * @param  string  $replace\n     * @param  string  $subject\n     * @return string\n     */\n    function str_replace_first($search, $replace, $subject)\n    {\n        return Str::replaceFirst($search, $replace, $subject);\n    }\n}\n\nif (!function_exists('str_replace_last')) {\n    /**\n     * str_replace_last replaces the last occurrence of a given value in the string\n     * @param  string  $search\n     * @param  string  $replace\n     * @param  string  $subject\n     * @return string\n     */\n    function str_replace_last($search, $replace, $subject)\n    {\n        return Str::replaceLast($search, $replace, $subject);\n    }\n}\n\nif (!function_exists('str_singular')) {\n    /**\n     * str_singular get the singular form of an English word\n     * @param  string  $value\n     * @return string\n     */\n    function str_singular($value)\n    {\n        return Str::singular($value);\n    }\n}\n\nif (!function_exists('str_slug')) {\n    /**\n     * str_slug generates a URL friendly \"slug\" from a given string\n     * @param  string  $title\n     * @param  string  $separator\n     * @param  string  $language\n     * @return string\n     */\n    function str_slug($title, $separator = '-', $language = 'en')\n    {\n        return Str::slug($title, $separator, $language);\n    }\n}\n\nif (!function_exists('str_start')) {\n    /**\n     * str_start begins a string with a single instance of a given value\n     * @param  string  $value\n     * @param  string  $prefix\n     * @return string\n     */\n    function str_start($value, $prefix)\n    {\n        return Str::start($value, $prefix);\n    }\n}\n\nif (!function_exists('studly_case')) {\n    /**\n     * studly_case converts a value to studly caps case\n     * @param  string  $value\n     * @return string\n     */\n    function studly_case($value)\n    {\n        return Str::studly($value);\n    }\n}\n\nif (!function_exists('title_case')) {\n    /**\n     * title_case converts a value to title case\n     * @param  string  $value\n     * @return string\n     */\n    function title_case($value)\n    {\n        return Str::title($value);\n    }\n}\n\nif (!function_exists('link_to')) {\n    /**\n     * Generate a HTML link.\n     *\n     * @param  string  $url\n     * @param  string  $title\n     * @param  array   $attributes\n     * @param  bool    $secure\n     * @return string\n     */\n    function link_to($url, $title = null, $attributes = [], $secure = null)\n    {\n        return app('html')->link($url, $title, $attributes, $secure);\n    }\n}\n\nif (!function_exists('link_to_asset')) {\n    /**\n     * Generate a HTML link to an asset.\n     *\n     * @param  string  $url\n     * @param  string  $title\n     * @param  array   $attributes\n     * @param  bool    $secure\n     * @return string\n     */\n    function link_to_asset($url, $title = null, $attributes = [], $secure = null)\n    {\n        return app('html')->linkAsset($url, $title, $attributes, $secure);\n    }\n}\n\nif (!function_exists('link_to_route')) {\n    /**\n     * Generate a HTML link to a named route.\n     *\n     * @param  string  $name\n     * @param  string  $title\n     * @param  array   $parameters\n     * @param  array   $attributes\n     * @return string\n     */\n    function link_to_route($name, $title = null, $parameters = [], $attributes = [])\n    {\n        return app('html')->linkRoute($name, $title, $parameters, $attributes);\n    }\n}\n\nif (!function_exists('link_to_action')) {\n    /**\n     * Generate a HTML link to a controller action.\n     *\n     * @param  string  $action\n     * @param  string  $title\n     * @param  array   $parameters\n     * @param  array   $attributes\n     * @return string\n     */\n    function link_to_action($action, $title = null, $parameters = [], $attributes = [])\n    {\n        return app('html')->linkAction($action, $title, $parameters, $attributes);\n    }\n}\n\nif (!function_exists('starts_with')) {\n    /**\n     * @deprecated use str_starts_with\n     */\n    function starts_with($haystack, $needles)\n    {\n        return Str::startsWith($haystack, $needles);\n    }\n}\n\nif (!function_exists('ends_with')) {\n    /**\n     * @deprecated use str_ends_with\n     */\n    function ends_with($haystack, $needles)\n    {\n        return Str::endsWith($haystack, $needles);\n    }\n}\n\nif (!function_exists('asset_version')) {\n    /**\n     * asset_version takes a disk path, resolves it to a public URL, and appends\n     * a cache-busting version query string based on the file's modification time.\n     *\n     * Supports path symbols: ~ (base), $ (plugins), # (themes)\n     *\n     *     asset_version('~/themes/demo/assets/js/app.js')\n     *     // → http://localhost/themes/demo/assets/js/app.js?v1a2b3c4d\n     *\n     * @param string $path\n     * @return string\n     */\n    function asset_version(string $path): string\n    {\n        return Url::assetVersion($path);\n    }\n}\n"
  },
  {
    "path": "init/init.php",
    "content": "<?php\n\nrequire __DIR__ . '/autoloader.php';\nrequire __DIR__ . '/functions.php';\nrequire __DIR__ . '/polyfills.php';\n"
  },
  {
    "path": "init/polyfills.php",
    "content": "<?php\n\n/**\n * URL constants as defined in the PHP Manual under \"Constants usable with\n * http_build_url()\".\n *\n * @see http://us2.php.net/manual/en/http.constants.php#http.constants.url\n */\nif (!defined('HTTP_URL_REPLACE')) {\n    define('HTTP_URL_REPLACE', 1);\n}\nif (!defined('HTTP_URL_JOIN_PATH')) {\n    define('HTTP_URL_JOIN_PATH', 2);\n}\nif (!defined('HTTP_URL_JOIN_QUERY')) {\n    define('HTTP_URL_JOIN_QUERY', 4);\n}\nif (!defined('HTTP_URL_STRIP_USER')) {\n    define('HTTP_URL_STRIP_USER', 8);\n}\nif (!defined('HTTP_URL_STRIP_PASS')) {\n    define('HTTP_URL_STRIP_PASS', 16);\n}\nif (!defined('HTTP_URL_STRIP_AUTH')) {\n    define('HTTP_URL_STRIP_AUTH', 32);\n}\nif (!defined('HTTP_URL_STRIP_PORT')) {\n    define('HTTP_URL_STRIP_PORT', 64);\n}\nif (!defined('HTTP_URL_STRIP_PATH')) {\n    define('HTTP_URL_STRIP_PATH', 128);\n}\nif (!defined('HTTP_URL_STRIP_QUERY')) {\n    define('HTTP_URL_STRIP_QUERY', 256);\n}\nif (!defined('HTTP_URL_STRIP_FRAGMENT')) {\n    define('HTTP_URL_STRIP_FRAGMENT', 512);\n}\nif (!defined('HTTP_URL_STRIP_ALL')) {\n    define('HTTP_URL_STRIP_ALL', 1024);\n}\n\nif (!function_exists('http_build_url')) {\n    /**\n     * Build a URL.\n     *\n     * The parts of the second URL will be merged into the first according to\n     * the flags argument.\n     *\n     * @see https://github.com/jakeasmith/http_build_url\n     *\n     * @param mixed $url     (part(s) of) an URL in form of a string or\n     *                       associative array like parse_url() returns\n     * @param mixed $parts   same as the first argument\n     * @param int   $flags   a bitmask of binary or'ed HTTP_URL constants;\n     *                       HTTP_URL_REPLACE is the default\n     * @param array $new_url if set, it will be filled with the parts of the\n     *                       composed url like parse_url() would return\n     * @author Jake A. Smith\n     * @return string\n     */\n    function http_build_url($url, $replace = [], $flags = HTTP_URL_REPLACE, &$newUrl = []): string\n    {\n        if (is_string($url)) {\n            $url = parse_url($url);\n        }\n        if (is_string($replace)) {\n            $replace = parse_url($replace);\n        }\n\n        $urlSegments = ['scheme', 'host', 'user', 'pass', 'port', 'path', 'query', 'fragment'];\n\n        // Set flags - HTTP_URL_STRIP_ALL and HTTP_URL_STRIP_AUTH cover several other flags.\n        if ($flags & HTTP_URL_STRIP_ALL) {\n            $flags |= HTTP_URL_STRIP_USER\n                   | HTTP_URL_STRIP_PASS\n                   | HTTP_URL_STRIP_PORT\n                   | HTTP_URL_STRIP_PATH\n                   | HTTP_URL_STRIP_QUERY\n                   | HTTP_URL_STRIP_FRAGMENT;\n        } elseif ($flags & HTTP_URL_STRIP_AUTH) {\n            $flags |= HTTP_URL_STRIP_USER\n                   | HTTP_URL_STRIP_PASS;\n        }\n\n        // Filter $url and $replace arrays to strip out unknown segments\n        array_change_key_case($url, CASE_LOWER);\n        array_change_key_case($replace, CASE_LOWER);\n\n        $url = array_filter($url, function ($value, $key) use ($urlSegments) {\n            return (in_array($key, $urlSegments) && isset($value));\n        }, ARRAY_FILTER_USE_BOTH);\n        $replace = array_filter($replace, function ($value, $key) use ($urlSegments) {\n            return (in_array($key, $urlSegments) && isset($value));\n        }, ARRAY_FILTER_USE_BOTH);\n\n        // Replace URL parts if required\n        if ($flags & HTTP_URL_REPLACE) {\n            $url = array_replace($url, $replace);\n        } else {\n            // Process joined paths\n            if (($flags & HTTP_URL_JOIN_PATH) && isset($replace['path'])) {\n                $urlPath = (isset($url['path'])) ? explode('/', trim($url['path'], '/')) : [];\n                $joinedPath = explode('/', trim($replace['path'], '/'));\n\n                $url['path'] = '/' . implode('/', array_merge($urlPath, $joinedPath));\n            }\n\n            // Process joined query string\n            if (($flags & HTTP_URL_JOIN_QUERY) && isset($replace['query'])) {\n                $urlQuery = $joinedQuery = [];\n\n                parse_str($url['query'] ?? '', $urlQuery);\n                parse_str($replace['query'] ?? '', $joinedQuery);\n\n                $url['query'] = http_build_query(array_replace_recursive($urlQuery, $joinedQuery));\n            }\n        }\n\n        // Strip segments as necessary\n        foreach ($urlSegments as $segment) {\n            $strip = 'HTTP_URL_STRIP_' . strtoupper($segment);\n\n            if (!defined($strip)) {\n                continue;\n            }\n\n            if ($flags & constant($strip)) {\n                unset($url[$segment]);\n            }\n        }\n\n        // Make new URL available\n        $newUrl = $url;\n\n        // Generate URL string\n        $urlString = '';\n\n        if (!empty($url['scheme'])) {\n            $urlString .= $url['scheme'] . '://';\n        }\n        if (!empty($url['user'])) {\n            $urlString .= $url['user'];\n\n            if (!empty($url['pass'])) {\n                $urlString .= ':' . $url['pass'];\n            }\n\n            $urlString .= '@';\n        }\n        if (!empty($url['host'])) {\n            $urlString .= $url['host'];\n        }\n        if (!empty($url['port'])) {\n            $urlString .= ':' . $url['port'];\n        }\n        if (!empty($url['path'])) {\n            $urlString .= ((substr($url['path'], 0, 1) !== '/') ? '/' : '') . $url['path'];\n        }\n        if (!empty($url['query'])) {\n            $urlString .= '?' . $url['query'];\n        }\n        if (!empty($url['fragment'])) {\n            $urlString .= '#' . $url['fragment'];\n        }\n\n        return $urlString;\n    }\n}\n"
  },
  {
    "path": "init.php",
    "content": "<?php\n\nrequire __DIR__ . '/init/autoloader.php';\nrequire __DIR__ . '/init/functions.php';\nrequire __DIR__ . '/init/polyfills.php';\n"
  },
  {
    "path": "phpbench.json",
    "content": "{\n    \"$schema\":\"./vendor/phpbench/phpbench/phpbench.schema.json\",\n    \"runner.bootstrap\": \"vendor/autoload.php\"\n}\n"
  },
  {
    "path": "phpcs.xml",
    "content": "<?xml version=\"1.0\"?>\n<ruleset name=\"October CMS\">\n    <description>The coding standard for October CMS.</description>\n    <rule ref=\"PSR2\">\n        <!--\n        Exceptions to the PSR-2 guidelines as per our Developer Guide:\n        https://octobercms.com/help/guidelines/developer#psr-exceptions\n        -->\n        <exclude name=\"PSR1.Methods.CamelCapsMethodName.NotCamelCaps\" />\n        <exclude name=\"Squiz.ControlStructures.ControlSignature.SpaceAfterCloseBrace\" />\n        <exclude name=\"PSR2.ControlStructures.ControlStructureSpacing.SpacingAfterOpenBrace\" />\n    </rule>\n\n    <rule ref=\"PSR1.Classes.ClassDeclaration.MissingNamespace\">\n        <!--\n        Migrations and tests do not need a namespace defined\n        -->\n        <exclude-pattern>*/src/Auth/Migrations/*\\.php</exclude-pattern>\n        <exclude-pattern>*/src/Database/Migrations/*\\.php</exclude-pattern>\n        <exclude-pattern>*/tests/*</exclude-pattern>\n    </rule>\n\n    <rule ref=\"PSR1.Classes.ClassDeclaration.MultipleClasses\">\n        <!--\n        Test fixtures and cases can have multiple classes defined, only if they are directly related to the test, or are\n        extended classes\n        -->\n        <exclude-pattern>*/tests/*</exclude-pattern>\n    </rule>\n\n    <file>src/</file>\n    <file>tests/</file>\n\n    <!-- Ignore vendor files -->\n    <exclude-pattern>*/vendor/*</exclude-pattern>\n    <!-- Ignore test config fixture -->\n    <exclude-pattern>*/tests/fixtures/config/sample-config.php</exclude-pattern>\n</ruleset>\n"
  },
  {
    "path": "phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit backupGlobals=\"false\"\n         backupStaticAttributes=\"false\"\n         colors=\"true\"\n         convertErrorsToExceptions=\"true\"\n         convertNoticesToExceptions=\"true\"\n         convertWarningsToExceptions=\"true\"\n         processIsolation=\"false\"\n         stopOnFailure=\"false\"\n>\n    <testsuites>\n        <testsuite name=\"October Rain Test Suite\">\n            <directory>./tests/**</directory>\n        </testsuite>\n    </testsuites>\n</phpunit>\n"
  },
  {
    "path": "src/Assetic/Asset/AssetCache.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Asset;\n\nuse October\\Rain\\Assetic\\Cache\\CacheInterface;\nuse October\\Rain\\Assetic\\Filter\\FilterInterface;\nuse October\\Rain\\Assetic\\Filter\\HashableInterface;\n\n/**\n * AssetCache caches an asset to avoid the cost of loading and dumping.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nclass AssetCache implements AssetInterface\n{\n    /**\n     * @var AssetInterface asset\n     */\n    protected $asset;\n\n    /**\n     * @var CacheInterface cache\n     */\n    protected $cache;\n\n    /**\n     * __construct\n     */\n    public function __construct(AssetInterface $asset, CacheInterface $cache)\n    {\n        $this->asset = $asset;\n        $this->cache = $cache;\n    }\n\n    /**\n     * ensureFilter\n     */\n    public function ensureFilter(FilterInterface $filter): void\n    {\n        $this->asset->ensureFilter($filter);\n    }\n\n    /**\n     * getFilters\n     */\n    public function getFilters(): array\n    {\n        return $this->asset->getFilters();\n    }\n\n    /**\n     * clearFilters\n     */\n    public function clearFilters(): void\n    {\n        $this->asset->clearFilters();\n    }\n\n    /**\n     * load\n     */\n    public function load(?FilterInterface $additionalFilter = null): void\n    {\n        $cacheKey = self::getCacheKey($this->asset, $additionalFilter, 'load');\n        if ($this->cache->has($cacheKey)) {\n            $this->asset->setContent($this->cache->get($cacheKey));\n\n            return;\n        }\n\n        $this->asset->load($additionalFilter);\n        $this->cache->set($cacheKey, $this->asset->getContent());\n    }\n\n    /**\n     * dump\n     */\n    public function dump(?FilterInterface $additionalFilter = null): string\n    {\n        $cacheKey = self::getCacheKey($this->asset, $additionalFilter, 'dump');\n        if ($this->cache->has($cacheKey)) {\n            return $this->cache->get($cacheKey);\n        }\n\n        $content = $this->asset->dump($additionalFilter);\n        $this->cache->set($cacheKey, $content);\n\n        return $content;\n    }\n\n    /**\n     * getContent\n     */\n    public function getContent(): ?string\n    {\n        return $this->asset->getContent();\n    }\n\n    /**\n     * setContent\n     */\n    public function setContent(?string $content): void\n    {\n        $this->asset->setContent($content);\n    }\n\n    /**\n     * getSourceRoot\n     */\n    public function getSourceRoot(): ?string\n    {\n        return $this->asset->getSourceRoot();\n    }\n\n    /**\n     * getSourcePath\n     */\n    public function getSourcePath(): ?string\n    {\n        return $this->asset->getSourcePath();\n    }\n\n    /**\n     * getSourceDirectory\n     */\n    public function getSourceDirectory(): ?string\n    {\n        return $this->asset->getSourceDirectory();\n    }\n\n    /**\n     * getTargetPath\n     */\n    public function getTargetPath(): ?string\n    {\n        return $this->asset->getTargetPath();\n    }\n\n    /**\n     * setTargetPath\n     */\n    public function setTargetPath(?string $targetPath): void\n    {\n        $this->asset->setTargetPath($targetPath);\n    }\n\n    /**\n     * getLastModified\n     */\n    public function getLastModified(): ?int\n    {\n        return $this->asset->getLastModified();\n    }\n\n    /**\n     * getVars\n     */\n    public function getVars(): array\n    {\n        return $this->asset->getVars();\n    }\n\n    /**\n     * setValues\n     */\n    public function setValues(array $values): void\n    {\n        $this->asset->setValues($values);\n    }\n\n    /**\n     * getValues\n     */\n    public function getValues(): array\n    {\n        return $this->asset->getValues();\n    }\n\n    /**\n     * getCacheKey returns a cache key for the current asset.\n     * The key is composed of everything but an asset's content:\n     *\n     *  * source root\n     *  * source path\n     *  * target url\n     *  * last modified\n     *  * filters\n     *\n     * @param AssetInterface  $asset            The asset\n     * @param FilterInterface $additionalFilter Any additional filter being applied\n     * @param string          $salt             Salt for the key\n     *\n     * @return string A key for identifying the current asset\n     */\n    protected static function getCacheKey(AssetInterface $asset, ?FilterInterface $additionalFilter = null, string $salt = ''): string\n    {\n        if ($additionalFilter) {\n            $asset = clone $asset;\n            $asset->ensureFilter($additionalFilter);\n        }\n\n        $cacheKey  = $asset->getSourceRoot();\n        $cacheKey .= $asset->getSourcePath();\n        $cacheKey .= $asset->getTargetPath();\n        $cacheKey .= $asset->getLastModified();\n\n        foreach ($asset->getFilters() as $filter) {\n            if ($filter instanceof HashableInterface) {\n                $cacheKey .= $filter->hash();\n            }\n            else {\n                $cacheKey .= serialize($filter);\n            }\n        }\n\n        if ($values = $asset->getValues()) {\n            asort($values);\n            $cacheKey .= serialize($values);\n        }\n\n        return md5($cacheKey.$salt);\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Asset/AssetCollection.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Asset;\n\nuse October\\Rain\\Assetic\\Asset\\Iterator\\AssetCollectionFilterIterator;\nuse October\\Rain\\Assetic\\Asset\\Iterator\\AssetCollectionIterator;\nuse October\\Rain\\Assetic\\Filter\\FilterCollection;\nuse October\\Rain\\Assetic\\Filter\\FilterInterface;\nuse RecursiveIteratorIterator;\nuse InvalidArgumentException;\nuse IteratorAggregate;\nuse SplObjectStorage;\nuse Traversable;\n\n/**\n * AssetCollection\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nclass AssetCollection implements IteratorAggregate, AssetCollectionInterface\n{\n    /**\n     * @var array assets\n     */\n    protected $assets;\n\n    /**\n     * @var FilterCollection filters\n     */\n    protected $filters;\n\n    /**\n     * @var string|null sourceRoot\n     */\n    protected $sourceRoot;\n\n    /**\n     * @var string|null targetPath\n     */\n    protected $targetPath;\n\n    /**\n     * @var string|null content\n     */\n    protected $content;\n\n    /**\n     * @var SplObjectStorage clones\n     */\n    protected $clones;\n\n    /**\n     * @var array vars\n     */\n    protected $vars;\n\n    /**\n     * @var array values\n     */\n    protected $values;\n\n    /**\n     * __construct\n     *\n     * @param array  $assets     Assets for the current collection\n     * @param array  $filters    Filters for the current collection\n     * @param string $sourceRoot The root directory\n     * @param array  $vars\n     */\n    public function __construct(array $assets = [], array $filters = [], ?string $sourceRoot = null, array $vars = [])\n    {\n        $this->assets = [];\n        foreach ($assets as $asset) {\n            $this->add($asset);\n        }\n\n        $this->filters = new FilterCollection($filters);\n        $this->sourceRoot = $sourceRoot;\n        $this->clones = new SplObjectStorage();\n        $this->vars = $vars;\n        $this->values = [];\n    }\n\n    /**\n     * __clone\n     */\n    public function __clone()\n    {\n        $this->filters = clone $this->filters;\n        $this->clones = new SplObjectStorage();\n    }\n\n    /**\n     * all\n     */\n    public function all(): array\n    {\n        return $this->assets;\n    }\n\n    /**\n     * add\n     */\n    public function add(AssetInterface $asset): void\n    {\n        $this->assets[] = $asset;\n    }\n\n    /**\n     * removeLeaf\n     */\n    public function removeLeaf(AssetInterface $needle, bool $graceful = false): bool\n    {\n        foreach ($this->assets as $i => $asset) {\n            $clone = isset($this->clones[$asset]) ? $this->clones[$asset] : null;\n            if (in_array($needle, [$asset, $clone], true)) {\n                unset($this->clones[$asset], $this->assets[$i]);\n\n                return true;\n            }\n\n            if ($asset instanceof AssetCollectionInterface && $asset->removeLeaf($needle, true)) {\n                return true;\n            }\n        }\n\n        if ($graceful) {\n            return false;\n        }\n\n        throw new InvalidArgumentException('Leaf not found.');\n    }\n\n    /**\n     * replaceLeaf\n     */\n    public function replaceLeaf(AssetInterface $needle, AssetInterface $replacement, bool $graceful = false): bool\n    {\n        foreach ($this->assets as $i => $asset) {\n            $clone = isset($this->clones[$asset]) ? $this->clones[$asset] : null;\n            if (in_array($needle, [$asset, $clone], true)) {\n                unset($this->clones[$asset]);\n                $this->assets[$i] = $replacement;\n\n                return true;\n            }\n\n            if ($asset instanceof AssetCollectionInterface && $asset->replaceLeaf($needle, $replacement, true)) {\n                return true;\n            }\n        }\n\n        if ($graceful) {\n            return false;\n        }\n\n        throw new InvalidArgumentException('Leaf not found.');\n    }\n\n    /**\n     * ensureFilter\n     */\n    public function ensureFilter(FilterInterface $filter): void\n    {\n        $this->filters->ensure($filter);\n    }\n\n    /**\n     * getFilters\n     */\n    public function getFilters(): array\n    {\n        return $this->filters->all();\n    }\n\n    /**\n     * clearFilters\n     */\n    public function clearFilters(): void\n    {\n        $this->filters->clear();\n        $this->clones = new SplObjectStorage();\n    }\n\n    /**\n     * load\n     */\n    public function load(?FilterInterface $additionalFilter = null): void\n    {\n        // loop through leaves and load each asset\n        $parts = [];\n        foreach ($this as $asset) {\n            $asset->load($additionalFilter);\n            $parts[] = $asset->getContent();\n        }\n\n        $this->content = implode(\"\\n\", $parts);\n    }\n\n    /**\n     * dump\n     */\n    public function dump(?FilterInterface $additionalFilter = null): string\n    {\n        // loop through leaves and dump each asset\n        $parts = [];\n        foreach ($this as $asset) {\n            $parts[] = $asset->dump($additionalFilter);\n        }\n\n        return implode(\"\\n\", $parts);\n    }\n\n    /**\n     * getContent\n     */\n    public function getContent(): ?string\n    {\n        return $this->content;\n    }\n\n    /**\n     * setContent\n     */\n    public function setContent(?string $content): void\n    {\n        $this->content = $content;\n    }\n\n    /**\n     * getSourceRoot\n     */\n    public function getSourceRoot(): ?string\n    {\n        return $this->sourceRoot;\n    }\n\n    /**\n     * getSourcePath\n     */\n    public function getSourcePath(): ?string\n    {\n        return null;\n    }\n\n    /**\n     * getSourceDirectory returns the first available source directory, useful\n     * when extracting imports and a singular collection is returned\n     */\n    public function getSourceDirectory(): ?string\n    {\n        foreach ($this as $asset) {\n            return $asset->getSourceDirectory();\n        }\n\n        return null;\n    }\n\n    /**\n     * getTargetPath\n     */\n    public function getTargetPath(): ?string\n    {\n        return $this->targetPath;\n    }\n\n    /**\n     * setTargetPath\n     */\n    public function setTargetPath(?string $targetPath): void\n    {\n        $this->targetPath = $targetPath;\n    }\n\n    /**\n     * getLastModified returns the highest last-modified value of all assets in the current collection.\n     *\n     * @return int|null A UNIX timestamp\n     */\n    public function getLastModified(): ?int\n    {\n        if (!count($this->assets)) {\n            return null;\n        }\n\n        $mtime = 0;\n        foreach ($this as $asset) {\n            $assetMtime = $asset->getLastModified();\n            if ($assetMtime > $mtime) {\n                $mtime = $assetMtime;\n            }\n        }\n\n        return $mtime;\n    }\n\n    /**\n     * getIterator returns an iterator for looping recursively over unique leaves.\n     */\n    public function getIterator(): Traversable\n    {\n        return new RecursiveIteratorIterator(new AssetCollectionFilterIterator(new AssetCollectionIterator($this, $this->clones)));\n    }\n\n    /**\n     * getVars\n     */\n    public function getVars(): array\n    {\n        return $this->vars;\n    }\n\n    /**\n     * setValues\n     */\n    public function setValues(array $values): void\n    {\n        $this->values = $values;\n\n        foreach ($this as $asset) {\n            $asset->setValues(array_intersect_key($values, array_flip($asset->getVars())));\n        }\n    }\n\n    /**\n     * getValues\n     */\n    public function getValues(): array\n    {\n        return $this->values;\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Asset/AssetCollectionInterface.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Asset;\n\n/**\n * AssetCollectionInterface is an asset collection.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\ninterface AssetCollectionInterface extends AssetInterface, \\Traversable\n{\n    /**\n     * Returns all child assets.\n     *\n     * @return array An array of AssetInterface objects\n     */\n    public function all(): array;\n\n    /**\n     * Adds an asset to the current collection.\n     *\n     * @param AssetInterface $asset An asset\n     */\n    public function add(AssetInterface $asset): void;\n\n    /**\n     * Removes a leaf.\n     *\n     * @param AssetInterface $leaf     The leaf to remove\n     * @param bool           $graceful Whether the failure should return false or throw an exception\n     *\n     * @return bool Whether the asset has been found\n     *\n     * @throws \\InvalidArgumentException If the asset cannot be found\n     */\n    public function removeLeaf(AssetInterface $leaf, bool $graceful = false): bool;\n\n    /**\n     * Replaces an existing leaf with a new one.\n     *\n     * @param AssetInterface $needle      The current asset to replace\n     * @param AssetInterface $replacement The new asset\n     * @param bool           $graceful    Whether the failure should return false or throw an exception\n     *\n     * @return bool Whether the asset has been found\n     *\n     * @throws \\InvalidArgumentException If the asset cannot be found\n     */\n    public function replaceLeaf(AssetInterface $needle, AssetInterface $replacement, bool $graceful = false): bool;\n}\n"
  },
  {
    "path": "src/Assetic/Asset/AssetInterface.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Asset;\n\nuse October\\Rain\\Assetic\\Filter\\FilterInterface;\n\n/**\n * AssetInterface is an asset has a mutable URL and content and can be loaded and dumped.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\ninterface AssetInterface\n{\n    /**\n     * Ensures the current asset includes the supplied filter.\n     *\n     * @param FilterInterface $filter A filter\n     */\n    public function ensureFilter(FilterInterface $filter): void;\n\n    /**\n     * Returns an array of filters currently applied.\n     *\n     * @return array An array of filters\n     */\n    public function getFilters(): array;\n\n    /**\n     * Clears all filters from the current asset.\n     */\n    public function clearFilters(): void;\n\n    /**\n     * Loads the asset into memory and applies load filters.\n     *\n     * You may provide an additional filter to apply during load.\n     *\n     * @param FilterInterface $additionalFilter An additional filter\n     */\n    public function load(?FilterInterface $additionalFilter = null): void;\n\n    /**\n     * Applies dump filters and returns the asset as a string.\n     *\n     * You may provide an additional filter to apply during dump.\n     *\n     * Dumping an asset should not change its state.\n     *\n     * If the current asset has not been loaded yet, it should be\n     * automatically loaded at this time.\n     *\n     * @param FilterInterface $additionalFilter An additional filter\n     *\n     * @return string The filtered content of the current asset\n     */\n    public function dump(?FilterInterface $additionalFilter = null): string;\n\n    /**\n     * Returns the loaded content of the current asset.\n     *\n     * @return string|null The content\n     */\n    public function getContent(): ?string;\n\n    /**\n     * Sets the content of the current asset.\n     *\n     * Filters can use this method to change the content of the asset.\n     *\n     * @param string|null $content The asset content\n     */\n    public function setContent(?string $content): void;\n\n    /**\n     * Returns an absolute path or URL to the source asset's root directory.\n     *\n     * This value should be an absolute path to a directory in the filesystem,\n     * an absolute URL with no path, or null.\n     *\n     * For example:\n     *\n     *  * '/path/to/web'\n     *  * 'http://example.com'\n     *  * null\n     *\n     * @return string|null The asset's root\n     */\n    public function getSourceRoot(): ?string;\n\n    /**\n     * Returns the relative path for the source asset.\n     *\n     * This value can be combined with the asset's source root (if both are\n     * non-null) to get something compatible with file_get_contents().\n     *\n     * For example:\n     *\n     *  * 'js/main.js'\n     *  * 'main.js'\n     *  * null\n     *\n     * @return string|null The source asset path\n     */\n    public function getSourcePath(): ?string;\n\n    /**\n     * Returns the asset's source directory.\n     *\n     * The source directory is the directory the asset was located in\n     * and can be used to resolve references relative to an asset.\n     *\n     * @return string|null The asset's source directory\n     */\n    public function getSourceDirectory(): ?string;\n\n    /**\n     * Returns the URL for the current asset.\n     *\n     * @return string|null A web URL where the asset will be dumped\n     */\n    public function getTargetPath(): ?string;\n\n    /**\n     * Sets the URL for the current asset.\n     *\n     * @param string|null $targetPath A web URL where the asset will be dumped\n     */\n    public function setTargetPath(?string $targetPath): void;\n\n    /**\n     * Returns the time the current asset was last modified.\n     *\n     * @return int|null A UNIX timestamp\n     */\n    public function getLastModified(): ?int;\n\n    /**\n     * Returns an array of variable names for this asset.\n     *\n     * @return array\n     */\n    public function getVars(): array;\n\n    /**\n     * Sets the values for the asset's variables.\n     *\n     * @param array $values\n     */\n    public function setValues(array $values): void;\n\n    /**\n     * Returns the current values for this asset.\n     *\n     * @return array an array of strings\n     */\n    public function getValues(): array;\n}\n"
  },
  {
    "path": "src/Assetic/Asset/BaseAsset.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Asset;\n\nuse October\\Rain\\Assetic\\Filter\\FilterCollection;\nuse October\\Rain\\Assetic\\Filter\\FilterInterface;\n\n/**\n * A base abstract asset.\n *\n * The methods load() and getLastModified() are left undefined, although a\n * reusable doLoad() method is available to child classes.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nabstract class BaseAsset implements AssetInterface\n{\n    /**\n     * @var FilterCollection filters\n     */\n    protected $filters;\n\n    /**\n     * @var string|null sourceRoot\n     */\n    protected $sourceRoot;\n\n    /**\n     * @var string|null sourcePath\n     */\n    protected $sourcePath;\n\n    /**\n     * @var string|null sourceDir\n     */\n    protected $sourceDir;\n\n    /**\n     * @var string|null targetPath\n     */\n    protected $targetPath;\n\n    /**\n     * @var string|null content\n     */\n    protected $content;\n\n    /**\n     * @var bool loaded\n     */\n    protected $loaded;\n\n    /**\n     * @var array vars\n     */\n    protected $vars;\n\n    /**\n     * @var array values\n     */\n    protected $values;\n\n    /**\n     * __construct\n     *\n     * @param array  $filters    Filters for the asset\n     * @param string $sourceRoot The root directory\n     * @param string $sourcePath The asset path\n     * @param array  $vars\n     */\n    public function __construct(array $filters = [], ?string $sourceRoot = null, ?string $sourcePath = null, array $vars = [])\n    {\n        $this->filters = new FilterCollection($filters);\n        $this->sourceRoot = $sourceRoot;\n        $this->sourcePath = $sourcePath;\n        if ($sourcePath && $sourceRoot) {\n            $this->sourceDir = dirname(\"$sourceRoot/$sourcePath\");\n        }\n        $this->vars = $vars;\n        $this->values = [];\n        $this->loaded = false;\n    }\n\n    public function __clone()\n    {\n        $this->filters = clone $this->filters;\n    }\n\n    public function ensureFilter(FilterInterface $filter): void\n    {\n        $this->filters->ensure($filter);\n    }\n\n    public function getFilters(): array\n    {\n        return $this->filters->all();\n    }\n\n    public function clearFilters(): void\n    {\n        $this->filters->clear();\n    }\n\n    /**\n     * Encapsulates asset loading logic.\n     *\n     * @param string          $content          The asset content\n     * @param FilterInterface $additionalFilter An additional filter\n     */\n    protected function doLoad(?string $content, ?FilterInterface $additionalFilter = null): void\n    {\n        $filter = clone $this->filters;\n        if ($additionalFilter) {\n            $filter->ensure($additionalFilter);\n        }\n\n        $asset = clone $this;\n        $asset->setContent($content);\n\n        $filter->filterLoad($asset);\n        $this->content = $asset->getContent();\n\n        $this->loaded = true;\n    }\n\n    public function dump(?FilterInterface $additionalFilter = null): string\n    {\n        if (!$this->loaded) {\n            $this->load();\n        }\n\n        $filter = clone $this->filters;\n        if ($additionalFilter) {\n            $filter->ensure($additionalFilter);\n        }\n\n        $asset = clone $this;\n        $filter->filterDump($asset);\n\n        return $asset->getContent() ?? '';\n    }\n\n    public function getContent(): ?string\n    {\n        return $this->content;\n    }\n\n    public function setContent(?string $content): void\n    {\n        $this->content = $content;\n    }\n\n    public function getSourceRoot(): ?string\n    {\n        return $this->sourceRoot;\n    }\n\n    public function getSourcePath(): ?string\n    {\n        return $this->sourcePath;\n    }\n\n    public function getSourceDirectory(): ?string\n    {\n        return $this->sourceDir;\n    }\n\n    public function getTargetPath(): ?string\n    {\n        return $this->targetPath;\n    }\n\n    public function setTargetPath(?string $targetPath): void\n    {\n        if ($this->vars) {\n            foreach ($this->vars as $var) {\n                if (false === strpos($targetPath, $var)) {\n                    throw new \\RuntimeException(sprintf('The asset target path \"%s\" must contain the variable \"{%s}\".', $targetPath, $var));\n                }\n            }\n        }\n\n        $this->targetPath = $targetPath;\n    }\n\n    public function getVars(): array\n    {\n        return $this->vars;\n    }\n\n    public function setValues(array $values): void\n    {\n        foreach ($values as $var => $v) {\n            if (!in_array($var, $this->vars, true)) {\n                throw new \\InvalidArgumentException(sprintf('The asset with source path \"%s\" has no variable named \"%s\".', $this->sourcePath, $var));\n            }\n        }\n\n        $this->values = $values;\n        $this->loaded = false;\n    }\n\n    public function getValues(): array\n    {\n        return $this->values;\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Asset/FileAsset.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Asset;\n\nuse File;\nuse October\\Rain\\Assetic\\Filter\\FilterInterface;\nuse October\\Rain\\Assetic\\Util\\VarUtils;\nuse InvalidArgumentException;\nuse RuntimeException;\n\n/**\n * FileAsset represents an asset loaded from a file.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nclass FileAsset extends BaseAsset\n{\n    /**\n     * @var string source path\n     */\n    protected $source;\n\n    /**\n     * __construct.\n     *\n     * @param string $source     An absolute path\n     * @param array  $filters    An array of filters\n     * @param string $sourceRoot The source asset root directory\n     * @param string $sourcePath The source asset path\n     * @param array  $vars\n     *\n     * @throws InvalidArgumentException If the supplied root doesn't match the source when guessing the path\n     */\n    public function __construct(string $source, array $filters = [], ?string $sourceRoot = null, ?string $sourcePath = null, array $vars = [])\n    {\n        if ($sourceRoot === null) {\n            $sourceRoot = dirname($source);\n            if ($sourcePath === null) {\n                $sourcePath = basename($source);\n            }\n        }\n        elseif ($sourcePath === null) {\n            if (strpos($source, $sourceRoot) !== 0) {\n                throw new InvalidArgumentException(sprintf('The source \"%s\" is not in the root directory \"%s\"', $source, $sourceRoot));\n            }\n\n            $sourcePath = substr($source, strlen($sourceRoot) + 1);\n        }\n\n        $this->source = $source;\n\n        parent::__construct($filters, $sourceRoot, $sourcePath, $vars);\n    }\n\n    /**\n     * load\n     */\n    public function load(?FilterInterface $additionalFilter = null): void\n    {\n        $source = VarUtils::resolve($this->source, $this->getVars(), $this->getValues());\n\n        if (!is_file($source)) {\n            throw new RuntimeException(sprintf('The source file \"%s\" does not exist.', File::nicePath($source)));\n        }\n\n        $this->doLoad(file_get_contents($source), $additionalFilter);\n    }\n\n    /**\n     * getLastModified\n     */\n    public function getLastModified(): ?int\n    {\n        $source = VarUtils::resolve($this->source, $this->getVars(), $this->getValues());\n\n        if (!is_file($source)) {\n            throw new RuntimeException(sprintf('The source file \"%s\" does not exist.', File::nicePath($source)));\n        }\n\n        return filemtime($source);\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Asset/GlobAsset.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Asset;\n\nuse October\\Rain\\Assetic\\Filter\\FilterInterface;\nuse October\\Rain\\Assetic\\Util\\VarUtils;\nuse Traversable;\n\n/**\n * GlobAsset is a collection of assets loaded by glob.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nclass GlobAsset extends AssetCollection\n{\n    /**\n     * @var array globs\n     */\n    protected $globs;\n\n    /**\n     * @var bool initialized\n     */\n    protected $initialized;\n\n    /**\n     * __construct\n     *\n     * @param string|array $globs   A single glob path or array of paths\n     * @param array        $filters An array of filters\n     * @param string       $root    The root directory\n     * @param array        $vars\n     */\n    public function __construct($globs, array $filters = [], ?string $root = null, array $vars = [])\n    {\n        $this->globs = (array) $globs;\n        $this->initialized = false;\n\n        parent::__construct([], $filters, $root, $vars);\n    }\n\n    /**\n     * all\n     */\n    public function all(): array\n    {\n        if (!$this->initialized) {\n            $this->initialize();\n        }\n\n        return parent::all();\n    }\n\n    /**\n     * load\n     */\n    public function load(?FilterInterface $additionalFilter = null): void\n    {\n        if (!$this->initialized) {\n            $this->initialize();\n        }\n\n        parent::load($additionalFilter);\n    }\n\n    /**\n     * dump\n     */\n    public function dump(?FilterInterface $additionalFilter = null): string\n    {\n        if (!$this->initialized) {\n            $this->initialize();\n        }\n\n        return parent::dump($additionalFilter);\n    }\n\n    /**\n     * getLastModified\n     */\n    public function getLastModified(): ?int\n    {\n        if (!$this->initialized) {\n            $this->initialize();\n        }\n\n        return parent::getLastModified();\n    }\n\n    /**\n     * getIterator\n     */\n    public function getIterator(): Traversable\n    {\n        if (!$this->initialized) {\n            $this->initialize();\n        }\n\n        return parent::getIterator();\n    }\n\n    /**\n     * setValues\n     */\n    public function setValues(array $values): void\n    {\n        parent::setValues($values);\n        $this->initialized = false;\n    }\n\n    /**\n     * initialize the collection based on the glob(s) passed in.\n     */\n    private function initialize()\n    {\n        foreach ($this->globs as $glob) {\n            $glob = VarUtils::resolve($glob, $this->getVars(), $this->getValues());\n\n            if (false !== $paths = glob($glob)) {\n                foreach ($paths as $path) {\n                    if (is_file($path)) {\n                        $asset = new FileAsset($path, [], $this->getSourceRoot(), null, $this->getVars());\n                        $asset->setValues($this->getValues());\n                        $this->add($asset);\n                    }\n                }\n            }\n        }\n\n        $this->initialized = true;\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Asset/HttpAsset.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Asset;\n\nuse October\\Rain\\Assetic\\Filter\\FilterInterface;\nuse October\\Rain\\Assetic\\Util\\VarUtils;\nuse InvalidArgumentException;\nuse RuntimeException;\n\n/**\n * HttpAsset represents an asset loaded via an HTTP request.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nclass HttpAsset extends BaseAsset\n{\n    /**\n     * @var string sourceUrl\n     */\n    protected $sourceUrl;\n\n    /**\n     * @var bool ignoreErrors\n     */\n    protected $ignoreErrors;\n\n    /**\n     * __construct.\n     *\n     * @param string  $sourceUrl    The source URL\n     * @param array   $filters      An array of filters\n     * @param bool    $ignoreErrors\n     * @param array   $vars\n     *\n     * @throws InvalidArgumentException If the first argument is not an URL\n     */\n    public function __construct(string $sourceUrl, array $filters = [], bool $ignoreErrors = false, array $vars = [])\n    {\n        if (strpos($sourceUrl, '//') === 0) {\n            $sourceUrl = 'http:'.$sourceUrl;\n        }\n        elseif (strpos($sourceUrl, '://') === false) {\n            throw new InvalidArgumentException(sprintf('\"%s\" is not a valid URL.', $sourceUrl));\n        }\n\n        $this->sourceUrl = $sourceUrl;\n        $this->ignoreErrors = $ignoreErrors;\n\n        [$scheme, $url] = explode('://', $sourceUrl, 2);\n        [$host, $path] = explode('/', $url, 2);\n\n        parent::__construct($filters, $scheme.'://'.$host, $path, $vars);\n    }\n\n    /**\n     * load\n     */\n    public function load(?FilterInterface $additionalFilter = null): void\n    {\n        $content = @file_get_contents(\n            VarUtils::resolve($this->sourceUrl, $this->getVars(), $this->getValues())\n        );\n\n        if (false === $content && !$this->ignoreErrors) {\n            throw new RuntimeException(sprintf('Unable to load asset from URL \"%s\"', $this->sourceUrl));\n        }\n\n        $this->doLoad($content, $additionalFilter);\n    }\n\n    /**\n     * getLastModified\n     */\n    public function getLastModified(): ?int\n    {\n        if (false !== @file_get_contents($this->sourceUrl, false, stream_context_create(['http' => ['method' => 'HEAD']]))) {\n            foreach ($http_response_header as $header) {\n                if (stripos($header, 'Last-Modified: ') === 0) {\n                    [, $mtime] = explode(':', $header, 2);\n\n                    return strtotime(trim($mtime));\n                }\n            }\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Asset/Iterator/AssetCollectionFilterIterator.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Asset\\Iterator;\n\nuse RecursiveFilterIterator;\n\n/**\n * AssetCollectionFilterIterator collection filter iterator.\n *\n * The filter iterator is responsible for de-duplication of leaf assets based\n * on both strict equality and source URL.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nclass AssetCollectionFilterIterator extends RecursiveFilterIterator\n{\n    protected $visited;\n    protected $sources;\n\n    /**\n     * Constructor.\n     *\n     * @param AssetCollectionIterator $iterator The inner iterator\n     * @param array                   $visited  An array of visited asset objects\n     * @param array                   $sources  An array of visited source strings\n     */\n    public function __construct(AssetCollectionIterator $iterator, array $visited = [], array $sources = [])\n    {\n        parent::__construct($iterator);\n\n        $this->visited = $visited;\n        $this->sources = $sources;\n    }\n\n    /**\n     * Determines whether the current asset is a duplicate.\n     *\n     * De-duplication is performed based on either strict equality or by\n     * matching sources.\n     *\n     * @return Boolean Returns true if we have not seen this asset yet\n     */\n    public function accept(): bool\n    {\n        $asset = $this->getInnerIterator()->current(true);\n        $duplicate = false;\n\n        // check strict equality\n        if (in_array($asset, $this->visited, true)) {\n            $duplicate = true;\n        } else {\n            $this->visited[] = $asset;\n        }\n\n        // check source\n        $sourceRoot = $asset->getSourceRoot();\n        $sourcePath = $asset->getSourcePath();\n        if ($sourceRoot && $sourcePath) {\n            $source = $sourceRoot.'/'.$sourcePath;\n            if (in_array($source, $this->sources)) {\n                $duplicate = true;\n            } else {\n                $this->sources[] = $source;\n            }\n        }\n\n        return !$duplicate;\n    }\n\n    /**\n     * Passes visited objects and source URLs to the child iterator.\n     */\n    public function getChildren(): ?RecursiveFilterIterator\n    {\n        return new self($this->getInnerIterator()->getChildren(), $this->visited, $this->sources);\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Asset/Iterator/AssetCollectionIterator.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Asset\\Iterator;\n\nuse October\\Rain\\Assetic\\Asset\\AssetCollectionInterface;\nuse RecursiveIterator;\nuse SplObjectStorage;\n\n/**\n * AssetCollectionIterator iterates over an asset collection.\n *\n * The iterator is responsible for cascading filters and target URL patterns\n * from parent to child assets.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nclass AssetCollectionIterator implements RecursiveIterator\n{\n    /**\n     * @var mixed assets\n     */\n    protected $assets;\n\n    /**\n     * @var mixed filters\n     */\n    protected $filters;\n\n    /**\n     * @var mixed vars\n     */\n    protected $vars;\n\n    /**\n     * @var mixed output\n     */\n    protected $output;\n\n    /**\n     * @var mixed clones\n     */\n    protected $clones;\n\n    /**\n     * __construct\n     */\n    public function __construct(AssetCollectionInterface $coll, SplObjectStorage $clones)\n    {\n        $this->assets  = $coll->all();\n        $this->filters = $coll->getFilters();\n        $this->vars    = $coll->getVars();\n        $this->output  = $coll->getTargetPath();\n        $this->clones  = $clones;\n\n        if (false === $pos = strrpos($this->output, '.')) {\n            $this->output .= '_*';\n        }\n        else {\n            $this->output = substr($this->output, 0, $pos).'_*'.substr($this->output, $pos);\n        }\n    }\n\n    /**\n     * Returns a copy of the current asset with filters and a target URL applied.\n     *\n     * @param bool $raw Returns the unmodified asset if true\n     *\n     * @return \\October\\Rain\\Assetic\\Asset\\AssetInterface\n     */\n    public function current($raw = false): mixed\n    {\n        $asset = current($this->assets);\n\n        if ($raw) {\n            return $asset;\n        }\n\n        // clone once\n        if (!isset($this->clones[$asset])) {\n            $clone = $this->clones[$asset] = clone $asset;\n\n            // generate a target path based on asset name\n            $name = sprintf('%s_%d', pathinfo($asset->getSourcePath(), PATHINFO_FILENAME) ?: 'part', $this->key() + 1);\n\n            $name = $this->removeDuplicateVar($name);\n\n            $clone->setTargetPath(str_replace('*', $name, $this->output));\n        } else {\n            $clone = $this->clones[$asset];\n        }\n\n        // cascade filters\n        foreach ($this->filters as $filter) {\n            $clone->ensureFilter($filter);\n        }\n\n        return $clone;\n    }\n\n    public function key(): mixed\n    {\n        return key($this->assets);\n    }\n\n    public function next(): void\n    {\n        next($this->assets);\n    }\n\n    public function rewind(): void\n    {\n        reset($this->assets);\n    }\n\n    public function valid(): bool\n    {\n        return current($this->assets) !== false;\n    }\n\n    public function hasChildren(): bool\n    {\n        return current($this->assets) instanceof AssetCollectionInterface;\n    }\n\n    /**\n     * @uses current()\n     */\n    public function getChildren(): ?RecursiveIterator\n    {\n        return new self($this->current(), $this->clones);\n    }\n\n    private function removeDuplicateVar($name)\n    {\n        foreach ($this->vars as $var) {\n            $var = '{'.$var.'}';\n            if (false !== strpos($name, $var) && false !== strpos($this->output, $var)) {\n                $name = str_replace($var, '', $name);\n            }\n        }\n\n        return $name;\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Asset/StringAsset.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Asset;\n\nuse October\\Rain\\Assetic\\Filter\\FilterInterface;\n\n/**\n * StringAsset represents a string asset.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nclass StringAsset extends BaseAsset\n{\n    /**\n     * @var string string\n     */\n    private $string;\n\n    /**\n     * @var int|null lastModified\n     */\n    private $lastModified;\n\n    /**\n     * __construct\n     *\n     * @param string $content    The content of the asset\n     * @param array  $filters    Filters for the asset\n     * @param string $sourceRoot The source asset root directory\n     * @param string $sourcePath The source asset path\n     */\n    public function __construct(string $content, array $filters = [], ?string $sourceRoot = null, ?string $sourcePath = null)\n    {\n        $this->string = $content;\n\n        parent::__construct($filters, $sourceRoot, $sourcePath);\n    }\n\n    /**\n     * load\n     */\n    public function load(?FilterInterface $additionalFilter = null): void\n    {\n        $this->doLoad($this->string, $additionalFilter);\n    }\n\n    /**\n     * setLastModified\n     */\n    public function setLastModified(?int $lastModified): void\n    {\n        $this->lastModified = $lastModified;\n    }\n\n    /**\n     * getLastModified\n     */\n    public function getLastModified(): ?int\n    {\n        return $this->lastModified;\n    }\n}\n"
  },
  {
    "path": "src/Assetic/AssetManager.php",
    "content": "<?php namespace October\\Rain\\Assetic;\n\nuse October\\Rain\\Assetic\\Asset\\AssetInterface;\nuse InvalidArgumentException;\n\n/**\n * AssetManager manages assets.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nclass AssetManager\n{\n    /**\n     * @var array assets\n     */\n    protected $assets = [];\n\n    /**\n     * get an asset by name.\n     *\n     * @param string $name The asset name\n     * @return AssetInterface The asset\n     * @throws InvalidArgumentException If there is no asset by that name\n     */\n    public function get(string $name): AssetInterface\n    {\n        if (!isset($this->assets[$name])) {\n            throw new InvalidArgumentException(sprintf('There is no \"%s\" asset.', $name));\n        }\n\n        return $this->assets[$name];\n    }\n\n    /**\n     * has checks if the current asset manager has a certain asset.\n     *\n     * @param string $name an asset name\n     * @return bool True if the asset has been set, false if not\n     */\n    public function has(string $name): bool\n    {\n        return isset($this->assets[$name]);\n    }\n\n    /**\n     * set registers an asset to the current asset manager.\n     *\n     * @param string         $name  The asset name\n     * @param AssetInterface $asset The asset\n     * @throws InvalidArgumentException If the asset name is invalid\n     */\n    public function set(string $name, AssetInterface $asset): void\n    {\n        if (!ctype_alnum(str_replace('_', '', $name))) {\n            throw new InvalidArgumentException(sprintf('The name \"%s\" is invalid.', $name));\n        }\n\n        $this->assets[$name] = $asset;\n    }\n\n    /**\n     * getNames returns an array of asset names.\n     *\n     * @return array An array of asset names\n     */\n    public function getNames(): array\n    {\n        return array_keys($this->assets);\n    }\n\n    /**\n     * clear clears all assets.\n     */\n    public function clear(): void\n    {\n        $this->assets = [];\n    }\n}\n"
  },
  {
    "path": "src/Assetic/AssetWriter.php",
    "content": "<?php namespace October\\Rain\\Assetic;\n\nuse October\\Rain\\Assetic\\Asset\\AssetInterface;\nuse October\\Rain\\Assetic\\Util\\VarUtils;\nuse InvalidArgumentException;\nuse RuntimeException;\n\n/**\n * AssetWriter writes assets to the filesystem.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n * @author Johannes M. Schmitt <schmittjoh@gmail.com>\n */\nclass AssetWriter\n{\n    /**\n     * @var string dir\n     */\n    protected $dir;\n\n    /**\n     * @var array values\n     */\n    protected $values;\n\n    /**\n     * __construct\n     *\n     * @param string $dir\n     * @param array  $values\n     * @throws InvalidArgumentException\n     */\n    public function __construct(string $dir, array $values = [])\n    {\n        foreach ($values as $var => $vals) {\n            foreach ($vals as $value) {\n                if (!is_string($value)) {\n                    throw new InvalidArgumentException(sprintf('All variable values must be strings, but got %s for variable \"%s\".', json_encode($value), $var));\n                }\n            }\n        }\n\n        $this->dir = $dir;\n        $this->values = $values;\n    }\n\n    /**\n     * writeManagerAssets\n     */\n    public function writeManagerAssets(AssetManager $am): void\n    {\n        foreach ($am->getNames() as $name) {\n            $this->writeAsset($am->get($name));\n        }\n    }\n\n    /**\n     * writeAsset\n     */\n    public function writeAsset(AssetInterface $asset): void\n    {\n        foreach (VarUtils::getCombinations($asset->getVars(), $this->values) as $combination) {\n            $asset->setValues($combination);\n\n            static::write(\n                $this->dir.'/'.VarUtils::resolve(\n                    $asset->getTargetPath(),\n                    $asset->getVars(),\n                    $asset->getValues()\n                ),\n                $asset->dump()\n            );\n        }\n    }\n\n    /**\n     * write\n     */\n    protected static function write(string $path, string $contents): void\n    {\n        if (!is_dir($dir = dirname($path)) && false === @mkdir($dir, 0755, true)) {\n            throw new RuntimeException('Unable to create directory '.$dir);\n        }\n\n        if (false === @file_put_contents($path, $contents)) {\n            throw new RuntimeException('Unable to write file '.$path);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Assetic/AsseticServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Assetic;\n\nuse Illuminate\\Support\\ServiceProvider;\nuse Illuminate\\Contracts\\Support\\DeferrableProvider;\n\n/**\n * AsseticServiceProvider\n *\n * @package october/assetic\n * @author Alexey Bobkov, Samuel Georges\n */\nclass AsseticServiceProvider extends ServiceProvider implements DeferrableProvider\n{\n    /**\n     * Register the service provider.\n     */\n    public function register(): void\n    {\n        $this->app->singleton('assetic', function ($app) {\n            $combiner = new Combiner;\n            $combiner->setStoragePath(storage_path('cms/combiner/assets'));\n            $combiner->registerDefaultFilters();\n            return $combiner;\n        });\n    }\n\n    /**\n     * Provides the returned services.\n     */\n    public function provides(): array\n    {\n        return [\n            'assetic',\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Cache/CacheInterface.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Cache;\n\n/**\n * CacheInterface interface for a cache backend.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\ninterface CacheInterface\n{\n    /**\n     * Checks if the cache has a value for a key.\n     *\n     * @param string $key A unique key\n     *\n     * @return Boolean Whether the cache has a value for this key\n     */\n    public function has($key);\n\n    /**\n     * Returns the value for a key.\n     *\n     * @param string $key A unique key\n     *\n     * @return string|null The value in the cache\n     */\n    public function get($key);\n\n    /**\n     * Sets a value in the cache.\n     *\n     * @param string $key   A unique key\n     * @param string $value The value to cache\n     */\n    public function set($key, $value);\n\n    /**\n     * Removes a value from the cache.\n     *\n     * @param string $key A unique key\n     */\n    public function remove($key);\n}\n"
  },
  {
    "path": "src/Assetic/Cache/FilesystemCache.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Cache;\n\nuse File;\nuse October\\Rain\\Assetic\\Cache\\CacheInterface;\nuse RuntimeException;\n\n/**\n * Assetic Filesystem Cache\n * Inherits the base logic except new files have permissions set.\n *\n * @package october/parse\n * @author Alexey Bobkov, Samuel Georges\n */\nclass FilesystemCache implements CacheInterface\n{\n    /**\n     * @var string dir name for the file cache\n     */\n    protected $dir;\n\n    /**\n     * __construct\n     */\n    public function __construct($dir)\n    {\n        $this->dir = $dir;\n    }\n\n    /**\n     * has\n     */\n    public function has($key)\n    {\n        return file_exists($this->dir.'/'.$key);\n    }\n\n    /**\n     * get\n     */\n    public function get($key)\n    {\n        $path = $this->dir.'/'.$key;\n\n        if (!file_exists($path)) {\n            throw new RuntimeException('There is no cached value for '.$key);\n        }\n\n        return file_get_contents($path);\n    }\n\n    /**\n     * set\n     */\n    public function set($key, $value)\n    {\n        if (!is_dir($this->dir) && false === @mkdir($this->dir, 0755, true)) {\n            throw new RuntimeException('Unable to create directory '.$this->dir);\n        }\n\n        $path = $this->dir.'/'.$key;\n\n        if (false === @file_put_contents($path, $value)) {\n            throw new RuntimeException('Unable to write file '.$path);\n        }\n\n        File::chmod($path);\n    }\n\n    /**\n     * remove\n     */\n    public function remove($key)\n    {\n        $path = $this->dir.'/'.$key;\n\n        if (file_exists($path) && false === @unlink($path)) {\n            throw new RuntimeException('Unable to remove file '.$path);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Combiner.php",
    "content": "<?php namespace October\\Rain\\Assetic;\n\nuse File;\nuse October\\Rain\\Assetic\\Asset\\FileAsset;\nuse October\\Rain\\Assetic\\Asset\\AssetCache;\nuse October\\Rain\\Assetic\\Asset\\AssetCollection;\nuse October\\Rain\\Assetic\\Cache\\FilesystemCache;\n\n/**\n * Combiner helper class\n *\n * @package october/assetic\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Combiner\n{\n    use \\October\\Rain\\Assetic\\Traits\\HasDeepHasher;\n\n    /**\n     * @var string storagePath is the output folder for storing combined files.\n     */\n    protected $storagePath;\n\n    /**\n     * @var string localPath is the local path context to find assets.\n     */\n    protected $localPath;\n\n    /**\n     * @var array filters to apply to each file.\n     */\n    protected $filters = [];\n\n    /**\n     * @var array prodFilters filters to apply in production\n     */\n    protected $prodFilters = [];\n\n    /**\n     * parse\n     */\n    public function parse(array $assets, array $options = []): string\n    {\n        return $this->prepareCombiner($assets, $options)->dump();\n    }\n\n    /**\n     * prepareCombiner before dumping\n     */\n    public function prepareCombiner(array $assets, array $options = []): AssetCollection\n    {\n        $targetPath = $options['targetPath'] ?? null;\n        $production = $options['production'] ?? false;\n        $useCache = $options['useCache'] ?? true;\n        $deepHashKey = $options['deepHashKey'] ?? null;\n\n        if ($deepHashKey !== null) {\n            $this->setDeepHashKeyOnFilters($deepHashKey);\n        }\n\n        $files = [];\n        $filesSalt = null;\n        foreach ($assets as $asset) {\n            $filters = $this->getFilters(File::extension($asset), (bool) $production);\n\n            $path = File::symbolizePath($asset);\n            if (!file_exists($path) && file_exists($this->localPath . $asset)) {\n                $path = $this->localPath . $asset;\n            }\n\n            $files[] = new FileAsset($path, $filters, base_path());\n            $filesSalt .= $this->localPath . $asset;\n        }\n\n        $filesSalt = md5($filesSalt);\n        $collection = new AssetCollection($files, [], $filesSalt);\n        $collection->setTargetPath($targetPath);\n\n        if (!$useCache || $this->storagePath === null) {\n            return $collection;\n        }\n\n        if (!File::isDirectory($this->storagePath)) {\n            @File::makeDirectory($this->storagePath);\n        }\n\n        $cache = new FilesystemCache($this->storagePath);\n\n        $cachedFiles = [];\n        foreach ($files as $file) {\n            $cachedFiles[] = new AssetCache($file, $cache);\n        }\n\n        $cachedCollection = new AssetCollection($cachedFiles, [], $filesSalt);\n        $cachedCollection->setTargetPath($targetPath);\n\n        return $cachedCollection;\n    }\n\n    /**\n     * registerDefaultFilters\n     */\n    public function registerDefaultFilters(): void\n    {\n        // Default JavaScript filters\n        $this->registerFilter('js', new \\October\\Rain\\Assetic\\Filter\\JavascriptImporter);\n\n        // Default StyleSheet filters\n        $this->registerFilter('css', new \\October\\Rain\\Assetic\\Filter\\CssImportFilter);\n        $this->registerFilter(['css', 'less', 'scss'], new \\October\\Rain\\Assetic\\Filter\\CssRewriteFilter);\n        $this->registerFilter('less', new \\October\\Rain\\Assetic\\Filter\\LessCompiler);\n        $this->registerFilter('scss', new \\October\\Rain\\Assetic\\Filter\\ScssCompiler);\n\n        // Production filters\n        $this->registerFilter('js', new \\October\\Rain\\Assetic\\Filter\\JSMinFilter, true);\n        $this->registerFilter(['css', 'less', 'scss'], new \\October\\Rain\\Assetic\\Filter\\StylesheetMinify, true);\n    }\n\n    /**\n     * setStoragePath\n     */\n    public function setStoragePath(?string $path): void\n    {\n        $this->storagePath = $path;\n    }\n\n    /**\n     * setLocalPath\n     */\n    public function setLocalPath(?string $path): void\n    {\n        $this->localPath = $path;\n    }\n\n    //\n    // Filters\n    //\n\n    /**\n     * registerFilter to apply to the combining process.\n     * @param string|array $extension Extension name. Eg: css\n     * @param object $filter Collection of files to combine.\n     * @param bool $isProduction\n     * @return self\n     */\n    public function registerFilter($extension, $filter, $isProduction = false)\n    {\n        if (is_array($extension)) {\n            foreach ($extension as $_extension) {\n                $this->registerFilter($_extension, $filter);\n            }\n            return;\n        }\n\n        $extension = strtolower($extension);\n        $destination = $isProduction ? 'prodFilters' : 'filters';\n\n        if (!isset($this->$destination[$extension])) {\n            $this->$destination[$extension] = [];\n        }\n\n        if ($filter !== null) {\n            $this->$destination[$extension][] = $filter;\n        }\n\n        return $this;\n    }\n\n    /**\n     * resetFilters clears any registered filters.\n     * @param string $extension Extension name. Eg: css\n     * @return self\n     */\n    public function resetFilters($extension = null)\n    {\n        if ($extension === null) {\n            $this->filters = [];\n            $this->prodFilters = [];\n        }\n        else {\n            $this->filters[$extension] = [];\n            $this->prodFilters[$extension] = [];\n        }\n\n        return $this;\n    }\n\n    /**\n     * getFilters returns all defined filters for a given extension\n     */\n    public function getFilters(?string $extension = null, bool $isProduction = false): array\n    {\n        if ($isProduction) {\n            if ($extension === null) {\n                return array_merge($this->filters, $this->prodFilters);\n            }\n\n            return array_merge(\n                ($this->filters[$extension] ?? []),\n                ($this->prodFilters[$extension] ?? [])\n            );\n        }\n\n        if ($extension === null) {\n            return $this->filters;\n        }\n\n        return $this->filters[$extension] ?? [];\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Factory/AssetFactory.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Factory;\n\nuse October\\Rain\\Assetic\\Asset\\AssetCollection;\nuse October\\Rain\\Assetic\\Asset\\AssetCollectionInterface;\nuse October\\Rain\\Assetic\\Asset\\AssetInterface;\nuse October\\Rain\\Assetic\\Asset\\FileAsset;\nuse October\\Rain\\Assetic\\Asset\\GlobAsset;\nuse October\\Rain\\Assetic\\Asset\\HttpAsset;\nuse October\\Rain\\Assetic\\AssetManager;\nuse October\\Rain\\Assetic\\Filter\\DependencyExtractorInterface;\nuse October\\Rain\\Assetic\\FilterManager;\nuse LogicException;\n\n/**\n * AssetFactory creates asset objects.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nclass AssetFactory\n{\n    /**\n     * @var mixed root\n     */\n    protected $root;\n\n    /**\n     * @var mixed debug\n     */\n    protected $debug;\n\n    /**\n     * @var mixed output\n     */\n    protected $output;\n\n    /**\n     * @var mixed am\n     */\n    protected $am;\n\n    /**\n     * @var mixed fm\n     */\n    protected $fm;\n\n    /**\n     * __construct\n     *\n     * @param string  $root  The default root directory\n     * @param Boolean $debug Filters prefixed with a \"?\" will be omitted in debug mode\n     */\n    public function __construct($root, $debug = false)\n    {\n        $this->root = $root ? rtrim($root, '/') : '';\n        $this->debug = $debug;\n        $this->output = 'assetic/*';\n    }\n\n    /**\n     * Sets debug mode for the current factory.\n     *\n     * @param Boolean $debug Debug mode\n     */\n    public function setDebug($debug)\n    {\n        $this->debug = $debug;\n    }\n\n    /**\n     * Checks if the factory is in debug mode.\n     *\n     * @return Boolean Debug mode\n     */\n    public function isDebug()\n    {\n        return $this->debug;\n    }\n\n    /**\n     * Sets the default output string.\n     *\n     * @param string $output The default output string\n     */\n    public function setDefaultOutput($output)\n    {\n        $this->output = $output;\n    }\n\n    /**\n     * Returns the current asset manager.\n     *\n     * @return AssetManager|null The asset manager\n     */\n    public function getAssetManager()\n    {\n        return $this->am;\n    }\n\n    /**\n     * Sets the asset manager to use when creating asset references.\n     *\n     * @param AssetManager $am The asset manager\n     */\n    public function setAssetManager(AssetManager $am)\n    {\n        $this->am = $am;\n    }\n\n    /**\n     * Returns the current filter manager.\n     *\n     * @return FilterManager|null The filter manager\n     */\n    public function getFilterManager()\n    {\n        return $this->fm;\n    }\n\n    /**\n     * Sets the filter manager to use when adding filters.\n     *\n     * @param FilterManager $fm The filter manager\n     */\n    public function setFilterManager(FilterManager $fm)\n    {\n        $this->fm = $fm;\n    }\n\n    /**\n     * createAsset creates a new asset.\n     *\n     * Prefixing a filter name with a question mark will cause it to be\n     * omitted when the factory is in debug mode.\n     *\n     * Available options:\n     *\n     *  * output: An output string\n     *  * name:   An asset name for interpolation in output patterns\n     *  * debug:  Forces debug mode on or off for this asset\n     *  * root:   An array or string of more root directories\n     *\n     * @param array|string $inputs  An array of input strings\n     * @param array|string $filters An array of filter names\n     * @param array        $options An array of options\n     *\n     * @return AssetCollection An asset collection\n     */\n    public function createAsset($inputs = [], $filters = [], array $options = [])\n    {\n        if (!is_array($inputs)) {\n            $inputs = [$inputs];\n        }\n\n        if (!is_array($filters)) {\n            $filters = [$filters];\n        }\n\n        if (!isset($options['output'])) {\n            $options['output'] = $this->output;\n        }\n\n        if (!isset($options['vars'])) {\n            $options['vars'] = [];\n        }\n\n        if (!isset($options['debug'])) {\n            $options['debug'] = $this->debug;\n        }\n\n        if (!isset($options['root'])) {\n            $options['root'] = [$this->root];\n        }\n        else {\n            if (!is_array($options['root'])) {\n                $options['root'] = [$options['root']];\n            }\n\n            $options['root'][] = $this->root;\n        }\n\n        if (!isset($options['name'])) {\n            $options['name'] = $this->generateAssetName($inputs, $filters, $options);\n        }\n\n        $asset = $this->createAssetCollection([], $options);\n        $extensions = [];\n\n        // inner assets\n        foreach ($inputs as $input) {\n            if (is_array($input)) {\n                // nested formula\n                $asset->add($this->createAsset(...$input));\n            }\n            else {\n                $asset->add($this->parseInput($input, $options));\n                $extensions[pathinfo($input, PATHINFO_EXTENSION)] = true;\n            }\n        }\n\n        // filters\n        foreach ($filters as $filter) {\n            if ('?' != $filter[0]) {\n                $asset->ensureFilter($this->getFilter($filter));\n            }\n            elseif (!$options['debug']) {\n                $asset->ensureFilter($this->getFilter(substr($filter, 1)));\n            }\n        }\n\n        // append variables\n        if (!empty($options['vars'])) {\n            $toAdd = [];\n            foreach ($options['vars'] as $var) {\n                if (false !== strpos($options['output'], '{'.$var.'}')) {\n                    continue;\n                }\n\n                $toAdd[] = '{'.$var.'}';\n            }\n\n            if ($toAdd) {\n                $options['output'] = str_replace('*', '*.'.implode('.', $toAdd), $options['output']);\n            }\n        }\n\n        // append consensus extension if missing\n        if (1 == count($extensions) && !pathinfo($options['output'], PATHINFO_EXTENSION) && $extension = key($extensions)) {\n            $options['output'] .= '.'.$extension;\n        }\n\n        // output --> target url\n        $asset->setTargetPath(str_replace('*', $options['name'], $options['output']));\n\n        // Return as a collection\n        return $asset instanceof AssetCollectionInterface\n            ? $asset\n            : $this->createAssetCollection([$asset]);\n    }\n\n    /**\n     * generateAssetName\n     */\n    public function generateAssetName($inputs, $filters, $options = []): string\n    {\n        foreach (array_diff(array_keys($options), ['output', 'debug', 'root']) as $key) {\n            unset($options[$key]);\n        }\n\n        ksort($options);\n\n        return substr(sha1(serialize($inputs).serialize($filters).serialize($options)), 0, 7);\n    }\n\n    /**\n     * getLastModified\n     */\n    public function getLastModified(AssetInterface $asset)\n    {\n        $mtime = 0;\n        foreach ($asset instanceof AssetCollectionInterface ? $asset : [$asset] as $leaf) {\n            $mtime = max($mtime, $leaf->getLastModified());\n\n            if (!$filters = $leaf->getFilters()) {\n                continue;\n            }\n\n            $prevFilters = [];\n            foreach ($filters as $filter) {\n                $prevFilters[] = $filter;\n\n                if (!$filter instanceof DependencyExtractorInterface) {\n                    continue;\n                }\n\n                // Extract children from leaf after running all preceding filters\n                $clone = clone $leaf;\n                $clone->clearFilters();\n                foreach (array_slice($prevFilters, 0, -1) as $prevFilter) {\n                    $clone->ensureFilter($prevFilter);\n                }\n                $clone->load();\n\n                foreach ($filter->getChildren($this, $clone->getContent(), $clone->getSourceDirectory()) as $child) {\n                    $mtime = max($mtime, $this->getLastModified($child));\n                }\n            }\n        }\n\n        return $mtime;\n    }\n\n    /**\n     * Parses an input string string into an asset.\n     *\n     * The input string can be one of the following:\n     *\n     *  * An absolute URL: If the string contains \"://\" or starts with \"//\" it will be interpreted as an HTTP asset\n     *  * A glob:          If the string contains a \"*\" it will be interpreted as a glob\n     *  * A path:          Otherwise the string is interpreted as a filesystem path\n     *\n     * Both globs and paths will be absolute using the current root directory.\n     *\n     * @param string $input   An input string\n     * @param array  $options An array of options\n     *\n     * @return AssetInterface An asset\n     */\n    protected function parseInput($input, array $options = [])\n    {\n        if (false !== strpos($input, '://') || 0 === strpos($input, '//')) {\n            return $this->createHttpAsset($input, $options['vars']);\n        }\n        if (self::isAbsolutePath($input)) {\n            if ($root = self::findRootDir($input, $options['root'])) {\n                $path = ltrim(substr($input, strlen($root)), '/');\n            }\n            else {\n                $path = null;\n            }\n        }\n        else {\n            $root  = $this->root;\n            $path  = $input;\n            $input = $this->root.'/'.$path;\n        }\n\n        if (false !== strpos($input, '*')) {\n            return $this->createGlobAsset($input, $root, $options['vars']);\n        }\n\n        return $this->createFileAsset($input, $root, $path, $options['vars']);\n    }\n\n    /**\n     * createAssetCollection\n     */\n    protected function createAssetCollection(array $assets = [], array $options = [])\n    {\n        return new AssetCollection($assets, [], null, isset($options['vars']) ? $options['vars'] : []);\n    }\n\n    /**\n     * createHttpAsset\n     */\n    protected function createHttpAsset($sourceUrl, $vars)\n    {\n        return new HttpAsset($sourceUrl, [], false, $vars);\n    }\n\n    /**\n     * createGlobAsset\n     */\n    protected function createGlobAsset($glob, $root = null, $vars = [])\n    {\n        return new GlobAsset($glob, [], $root, $vars);\n    }\n\n    /**\n     * createFileAsset\n     */\n    protected function createFileAsset($source, $root = null, $path = null, $vars = [])\n    {\n        return new FileAsset($source, [], $root, $path, $vars);\n    }\n\n    /**\n     * getFilter\n     */\n    protected function getFilter($name)\n    {\n        if (!$this->fm) {\n            throw new LogicException('There is no filter manager.');\n        }\n\n        return $this->fm->get($name);\n    }\n\n    /**\n     * isAbsolutePath\n     */\n    private static function isAbsolutePath($path)\n    {\n        return '/' == $path[0] || '\\\\' == $path[0] || (3 < strlen($path) && ctype_alpha($path[0]) && $path[1] == ':' && ('\\\\' == $path[2] || '/' == $path[2]));\n    }\n\n    /**\n     * findRootDir loops through the root directories and returns the first match.\n     *\n     * @param string $path  An absolute path\n     * @param array  $roots An array of root directories\n     *\n     * @return string|null The matching root directory, if found\n     */\n    private static function findRootDir($path, array $roots)\n    {\n        foreach ($roots as $root) {\n            if (0 === strpos($path, $root)) {\n                return $root;\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Filter/BaseCssFilter.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Filter;\n\nuse October\\Rain\\Assetic\\Util\\CssUtils;\n\n/**\n * BaseCssFilter is an abstract filter for dealing with CSS.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nabstract class BaseCssFilter implements FilterInterface\n{\n    /**\n     * @see CssUtils::filterReferences()\n     */\n    protected function filterReferences(string $content, callable $callback): string\n    {\n        return CssUtils::filterReferences($content, $callback);\n    }\n\n    /**\n     * @see CssUtils::filterUrls()\n     */\n    protected function filterUrls(string $content, callable $callback): string\n    {\n        return CssUtils::filterUrls($content, $callback);\n    }\n\n    /**\n     * @see CssUtils::filterImports()\n     */\n    protected function filterImports(string $content, callable $callback, bool $includeUrl = true): string\n    {\n        return CssUtils::filterImports($content, $callback, $includeUrl);\n    }\n\n    /**\n     * @see CssUtils::filterIEFilters()\n     */\n    protected function filterIEFilters(string $content, callable $callback): string\n    {\n        return CssUtils::filterIEFilters($content, $callback);\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Filter/CssImportFilter.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Filter;\n\nuse October\\Rain\\Assetic\\Filter\\HashableInterface;\nuse October\\Rain\\Assetic\\Asset\\AssetInterface;\nuse October\\Rain\\Assetic\\Asset\\FileAsset;\nuse October\\Rain\\Assetic\\Asset\\HttpAsset;\nuse October\\Rain\\Assetic\\Factory\\AssetFactory;\nuse October\\Rain\\Assetic\\Util\\CssUtils;\n\n/**\n * CssImportFilter converts imported stylesheets to inline.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nclass CssImportFilter extends BaseCssFilter implements HashableInterface, DependencyExtractorInterface\n{\n    /**\n     * @var FilterInterface|null importFilter\n     */\n    protected $importFilter;\n\n    /**\n     * @var string|null lastHash\n     */\n    protected $lastHash;\n\n    /**\n     * __construct\n     *\n     * @param FilterInterface $importFilter Filter for each imported asset\n     */\n    public function __construct(?FilterInterface $importFilter = null)\n    {\n        $this->importFilter = $importFilter ?: new CssRewriteFilter();\n    }\n\n    /**\n     * filterLoad\n     */\n    public function filterLoad(AssetInterface $asset): void\n    {\n        $importFilter = $this->importFilter;\n        $sourceRoot = $asset->getSourceRoot();\n        $sourcePath = $asset->getSourcePath();\n\n        $callback = function ($matches) use ($importFilter, $sourceRoot, $sourcePath) {\n            if (!$matches['url'] || $sourceRoot === null) {\n                return $matches[0];\n            }\n\n            $importRoot = $sourceRoot;\n\n            // Absolute\n            if (strpos($matches['url'], '://') !== false) {\n                [$importScheme, $tmp] = explode('://', $matches['url'], 2);\n                [$importHost, $importPath] = explode('/', $tmp, 2);\n                $importRoot = $importScheme.'://'.$importHost;\n            }\n            // Protocol-relative\n            elseif (strpos($matches['url'], '//') === 0) {\n                [$importHost, $importPath] = explode('/', substr($matches['url'], 2), 2);\n                $importRoot = '//'.$importHost;\n            }\n            // Root-relative\n            elseif ($matches['url'][0] == '/') {\n                $importPath = substr($matches['url'], 1);\n            }\n            // Document-relative\n            elseif ($sourcePath !== null) {\n                $importPath = $matches['url'];\n                if ('.' != $sourceDir = dirname($sourcePath)) {\n                    $importPath = $sourceDir.'/'.$importPath;\n                }\n            }\n            else {\n                return $matches[0];\n            }\n\n            $importSource = $importRoot.'/'.$importPath;\n            if (strpos($importSource, '://') !== false || strpos($importSource, '//') === 0) {\n                $import = new HttpAsset($importSource, [$importFilter], true);\n            }\n            // Ignore non-css and non-existent imports\n            elseif (pathinfo($importPath, PATHINFO_EXTENSION) != 'css' || !file_exists($importSource)) {\n                return $matches[0];\n            }\n            else {\n                $import = new FileAsset($importSource, [$importFilter], $importRoot, $importPath);\n            }\n\n            $import->setTargetPath($sourcePath);\n\n            return $import->dump();\n        };\n\n        $content = $asset->getContent();\n        $lastHash = md5($content);\n\n        do {\n            $content = $this->filterImports($content, $callback);\n            $hash = md5($content);\n        } while ($lastHash != $hash && ($lastHash = $hash));\n\n        $asset->setContent($content);\n    }\n\n    /**\n     * filterDump\n     */\n    public function filterDump(AssetInterface $asset): void\n    {\n    }\n\n    /**\n     * hashAsset\n     */\n    public function hashAsset($asset, $localPath)\n    {\n        $factory = new AssetFactory($localPath);\n        $children = $this->getAllChildren($factory, file_get_contents($asset), dirname($asset));\n\n        $allFiles = [];\n        foreach ($children as $child) {\n            $allFiles[] = $child;\n        }\n\n        $modified = [];\n        foreach ($allFiles as $file) {\n            $modified[] = $file->getLastModified();\n        }\n\n        return md5(implode('|', $modified));\n    }\n\n    /**\n     * setHash\n     */\n    public function setHash($hash)\n    {\n        $this->lastHash = $hash;\n    }\n\n    /**\n     * hash generated for the object\n     * @return string\n     */\n    public function hash()\n    {\n        return $this->lastHash ?: serialize($this);\n    }\n\n    /**\n     * getAllChildren loads all children recursively\n     */\n    public function getAllChildren(AssetFactory $factory, $content, $loadPath = null)\n    {\n        $children = (new static)->getChildren($factory, $content, $loadPath);\n\n        foreach ($children as $child) {\n            $childContent = file_get_contents($child->getSourceRoot().'/'.$child->getSourcePath());\n            $children = array_merge($children, (new static)->getChildren($factory, $childContent, $loadPath.'/'.dirname($child->getSourcePath())));\n        }\n\n        return $children;\n    }\n\n    /**\n     * getChildren only returns one level of children\n     */\n    public function getChildren(AssetFactory $factory, $content, $loadPath = null)\n    {\n        if (!$loadPath) {\n            return [];\n        }\n\n        $children = [];\n        foreach (CssUtils::extractImports($content) as $reference) {\n            // Strict check, only allow .css imports\n            if (substr($reference, -4) !== '.css') {\n                continue;\n            }\n\n            if (file_exists($file = $loadPath.'/'.$reference)) {\n                $coll = $factory->createAsset($file, [], ['root' => $loadPath]);\n                foreach ($coll as $leaf) {\n                    $leaf->ensureFilter($this);\n                    $children[] = $leaf;\n                    break;\n                }\n            }\n        }\n\n        return $children;\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Filter/CssMinFilter.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Filter;\n\nuse October\\Rain\\Assetic\\Asset\\AssetInterface;\n\n/**\n * CssMinFilter filters assets through CssMin.\n *\n * @link http://code.google.com/p/cssmin\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nclass CssMinFilter implements FilterInterface\n{\n    private $filters;\n    private $plugins;\n\n    public function __construct()\n    {\n        $this->filters = [];\n        $this->plugins = [];\n    }\n\n    public function setFilters(array $filters)\n    {\n        $this->filters = $filters;\n    }\n\n    public function setFilter($name, $value)\n    {\n        $this->filters[$name] = $value;\n    }\n\n    public function setPlugins(array $plugins)\n    {\n        $this->plugins = $plugins;\n    }\n\n    public function setPlugin($name, $value)\n    {\n        $this->plugins[$name] = $value;\n    }\n\n    public function filterLoad(AssetInterface $asset): void\n    {\n    }\n\n    public function filterDump(AssetInterface $asset): void\n    {\n        $filters = $this->filters;\n        $plugins = $this->plugins;\n\n        if (isset($filters['ImportImports']) && true === $filters['ImportImports']) {\n            if ($dir = $asset->getSourceDirectory()) {\n                $filters['ImportImports'] = array('BasePath' => $dir);\n            } else {\n                unset($filters['ImportImports']);\n            }\n        }\n\n        $asset->setContent(\\CssMin::minify($asset->getContent(), $filters, $plugins));\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Filter/CssRewriteFilter.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Filter;\n\nuse October\\Rain\\Assetic\\Asset\\AssetInterface;\n\n/**\n * CssRewriteFilter fixes relative CSS urls.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nclass CssRewriteFilter extends BaseCssFilter\n{\n    /**\n     * filterLoad\n     */\n    public function filterLoad(AssetInterface $asset): void\n    {\n    }\n\n    /**\n     * filterDump\n     */\n    public function filterDump(AssetInterface $asset): void\n    {\n        $sourceBase = $asset->getSourceRoot();\n        $sourcePath = $asset->getSourcePath();\n        $targetPath = $asset->getTargetPath();\n\n        if ($sourcePath === null || $targetPath === null || $sourcePath == $targetPath) {\n            return;\n        }\n\n        // Learn how to get from the target back to the source\n        if (strpos($sourceBase, '://') !== false) {\n            [$scheme, $url] = explode('://', $sourceBase.'/'.$sourcePath, 2);\n            [$host, $path] = explode('/', $url, 2);\n\n            $host = $scheme.'://'.$host.'/';\n            $path = false === strpos($path, '/') ? '' : dirname($path);\n            $path .= '/';\n        }\n        else {\n            // Assume source and target are on the same host\n            $host = '';\n\n            // Pop entries off the target until it fits in the source\n            if (dirname($sourcePath) == '.') {\n                $path = str_repeat('../', substr_count($targetPath, '/'));\n            }\n            elseif (($targetDir = dirname($targetPath)) == '.') {\n                $path = dirname($sourcePath).'/';\n            }\n            else {\n                $path = '';\n                while (strpos($sourcePath, $targetDir) !== 0) {\n                    if (($pos = strrpos($targetDir, '/')) !== false) {\n                        $targetDir = substr($targetDir, 0, $pos);\n                        $path .= '../';\n                    }\n                    else {\n                        $targetDir = '';\n                        $path .= '../';\n                        break;\n                    }\n                }\n                $path .= ltrim(substr(dirname($sourcePath).'/', strlen($targetDir)), '/');\n            }\n        }\n\n        $content = $this->filterReferences($asset->getContent(), function ($matches) use ($host, $path) {\n            // Absolute or protocol-relative or data uri\n            if (\n                strpos($matches['url'], '://') !== false ||\n                strpos($matches['url'], '#') === 0 ||\n                strpos($matches['url'], '//') === 0 ||\n                strpos($matches['url'], 'data:') === 0\n            ) {\n                return $matches[0];\n            }\n\n            // Root relative\n            if (isset($matches['url'][0]) && '/' == $matches['url'][0]) {\n                return str_replace($matches['url'], $host.$matches['url'], $matches[0]);\n            }\n\n            // Document relative\n            $url = $matches['url'];\n            while (strpos($url, '../') === 0 && substr_count($path, '/') >= 2) {\n                $path = substr($path, 0, strrpos(rtrim($path, '/'), '/') + 1);\n                $url = substr($url, 3);\n            }\n\n            $parts = [];\n            foreach (explode('/', $host.$path.$url) as $part) {\n                if ($part === '..' && count($parts) && end($parts) !== '..') {\n                    array_pop($parts);\n                }\n                else {\n                    $parts[] = $part;\n                }\n            }\n\n            return str_replace($matches['url'], implode('/', $parts), $matches[0]);\n        });\n\n        $asset->setContent($content);\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Filter/DependencyExtractorInterface.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Filter;\n\nuse October\\Rain\\Assetic\\Asset\\AssetInterface;\nuse October\\Rain\\Assetic\\Factory\\AssetFactory;\n\n/**\n * DependencyExtractorInterface is a filter that knows how to extract dependencies.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\ninterface DependencyExtractorInterface extends FilterInterface\n{\n    /**\n     * Returns child assets.\n     *\n     * @param AssetFactory $factory  The asset factory\n     * @param string       $content  The asset content\n     * @param string       $loadPath An optional load path\n     *\n     * @return AssetInterface[] Child assets\n     */\n    public function getChildren(AssetFactory $factory, $content, $loadPath = null);\n}\n"
  },
  {
    "path": "src/Assetic/Filter/FilterCollection.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Filter;\n\nuse October\\Rain\\Assetic\\Asset\\AssetInterface;\nuse Traversable;\n\n/**\n * FilterCollection is a collection of filters.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nclass FilterCollection implements FilterInterface, \\IteratorAggregate, \\Countable\n{\n    /**\n     * @var array filters\n     */\n    protected $filters = [];\n\n    /**\n     * __construct\n     */\n    public function __construct(array $filters = [])\n    {\n        foreach ($filters as $filter) {\n            $this->ensure($filter);\n        }\n    }\n\n    /**\n     * Checks that the current collection contains the supplied filter.\n     *\n     * If the supplied filter is another filter collection, each of its\n     * filters will be checked.\n     */\n    public function ensure(FilterInterface $filter): void\n    {\n        if ($filter instanceof \\Traversable) {\n            foreach ($filter as $f) {\n                $this->ensure($f);\n            }\n        } elseif (!in_array($filter, $this->filters, true)) {\n            $this->filters[] = $filter;\n        }\n    }\n\n    /**\n     * all\n     */\n    public function all(): array\n    {\n        return $this->filters;\n    }\n\n    /**\n     * clear\n     */\n    public function clear(): void\n    {\n        $this->filters = [];\n    }\n\n    /**\n     * filterLoad\n     */\n    public function filterLoad(AssetInterface $asset): void\n    {\n        foreach ($this->filters as $filter) {\n            $filter->filterLoad($asset);\n        }\n    }\n\n    /**\n     * filterDump\n     */\n    public function filterDump(AssetInterface $asset): void\n    {\n        foreach ($this->filters as $filter) {\n            $filter->filterDump($asset);\n        }\n    }\n\n    /**\n     * getIterator\n     */\n    public function getIterator(): Traversable\n    {\n        return new \\ArrayIterator($this->filters);\n    }\n\n    /**\n     * count\n     */\n    public function count(): int\n    {\n        return count($this->filters);\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Filter/FilterInterface.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Filter;\n\nuse October\\Rain\\Assetic\\Asset\\AssetInterface;\n\n/**\n * A filter manipulates an asset at load and dump.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\ninterface FilterInterface\n{\n    /**\n     * Filters an asset after it has been loaded.\n     *\n     * @param AssetInterface $asset An asset\n     */\n    public function filterLoad(AssetInterface $asset): void;\n\n    /**\n     * Filters an asset just before it's dumped.\n     *\n     * @param AssetInterface $asset An asset\n     */\n    public function filterDump(AssetInterface $asset): void;\n}\n"
  },
  {
    "path": "src/Assetic/Filter/HashableInterface.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Filter;\n\n/**\n * A filter can implement a hash function\n *\n * @author Francisco Facioni <fran6co@gmail.com>\n */\ninterface HashableInterface\n{\n    /**\n     * Generates a hash for the object\n     *\n     * @return string Object hash\n     */\n    public function hash();\n}\n"
  },
  {
    "path": "src/Assetic/Filter/JSMinFilter.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Filter;\n\n\nuse JSMin;\nuse October\\Rain\\Assetic\\Asset\\AssetInterface;\n\n/**\n * JSMinFilter filters assets through JsMin.\n *\n * All credit for the filter itself is mentioned in the file itself.\n *\n * @link https://raw.github.com/mrclay/minify/master/min/lib/JSMin.php\n * @author Brunoais <brunoaiss@gmail.com>\n */\nclass JSMinFilter implements FilterInterface\n{\n    /**\n     * filterLoad\n     */\n    public function filterLoad(AssetInterface $asset): void\n    {\n    }\n\n    /**\n     * filterDump will use JSMin to minify the asset and checks the filename\n     * for \"min.js\" to issues arising from double minification.\n     */\n    public function filterDump(AssetInterface $asset): void\n    {\n        $contents = $asset->getContent();\n\n        $isMinifiedAlready = strpos($asset->getSourcePath(), '.min.js') !== false;\n        if (!$isMinifiedAlready) {\n            $contents = JSMin::minify($contents);\n        }\n\n        $asset->setContent($contents);\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Filter/JSqueezeFilter.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Filter;\n\n\nuse October\\Rain\\Assetic\\Asset\\AssetInterface;\n\n/**\n * JSqueeze filter.\n *\n * @link https://github.com/nicolas-grekas/JSqueeze\n * @author Nicolas Grekas <p@tchwork.com>\n */\nclass JSqueezeFilter implements FilterInterface\n{\n    /**\n     * @var mixed singleLine\n     */\n    protected $singleLine = true;\n\n    /**\n     * @var bool keepImportantComments\n     */\n    protected $keepImportantComments = true;\n\n    /**\n     * @var mixed className\n     */\n    protected $className;\n\n    /**\n     * @var bool specialVarRx\n     */\n    protected $specialVarRx = false;\n\n    /**\n     * @var mixed defaultRx\n     */\n    protected $defaultRx;\n\n    /**\n     * __construct\n     */\n    public function __construct()\n    {\n        // JSqueeze is namespaced since 2.x, this works with both 1.x and 2.x\n        if (class_exists('\\\\Patchwork\\\\JSqueeze')) {\n            $this->className = '\\\\Patchwork\\\\JSqueeze';\n            $this->defaultRx = \\Patchwork\\JSqueeze::SPECIAL_VAR_PACKER;\n        } else {\n            $this->className = '\\\\JSqueeze';\n            $this->defaultRx = \\JSqueeze::SPECIAL_VAR_RX;\n        }\n    }\n\n    public function setSingleLine($bool)\n    {\n        $this->singleLine = (bool) $bool;\n    }\n\n    // call setSpecialVarRx(true) to enable global var/method/property\n    // renaming with the default regex (for 1.x or 2.x)\n    public function setSpecialVarRx($specialVarRx)\n    {\n        if (true === $specialVarRx) {\n            $this->specialVarRx = $this->defaultRx;\n        } else {\n            $this->specialVarRx = $specialVarRx;\n        }\n    }\n\n    public function keepImportantComments($bool)\n    {\n        $this->keepImportantComments = (bool) $bool;\n    }\n\n    public function filterLoad(AssetInterface $asset): void\n    {\n    }\n\n    public function filterDump(AssetInterface $asset): void\n    {\n        $parser = new $this->className();\n        $asset->setContent($parser->squeeze(\n            $asset->getContent(),\n            $this->singleLine,\n            $this->keepImportantComments,\n            $this->specialVarRx\n        ));\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Filter/JavascriptImporter.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Filter;\n\nuse File;\nuse October\\Rain\\Assetic\\Asset\\AssetInterface;\nuse October\\Rain\\Assetic\\Filter\\FilterInterface;\nuse RuntimeException;\n\n/**\n * JavascriptImporter importer JS Filter\n * Class used to import referenced javascript files, inside comments.\n *\n * =include library/jquery.js;\n * =require library/jquery.js;\n *\n * @package october/parse\n * @author Alexey Bobkov, Samuel Georges\n */\nclass JavascriptImporter implements FilterInterface\n{\n    /**\n     * @var string Location of where the processed JS script resides.\n     */\n    protected $scriptPath;\n\n    /**\n     * @var string File name for the processed JS script.\n     */\n    protected $scriptFile;\n\n    /**\n     * @var array Cache of required files.\n     */\n    protected $includedFiles = [];\n\n    /**\n     * @var array Variables defined by this script.\n     */\n    protected $definedVars = [];\n\n    /**\n     * filterLoad\n     */\n    public function filterLoad(AssetInterface $asset): void\n    {\n    }\n\n    /**\n     * filterDump\n     */\n    public function filterDump(AssetInterface $asset): void\n    {\n        $this->scriptPath = dirname($asset->getSourceRoot() . '/' . $asset->getSourcePath());\n        $this->scriptFile = basename($asset->getSourcePath());\n\n        $asset->setContent($this->parse($asset->getContent()));\n    }\n\n    /**\n     * Process JS imports inside a string of javascript\n     * @param string $content JS code to process.\n     * @return string Processed JS.\n     */\n    protected function parse(string $content): string\n    {\n        $macros = [];\n        $imported = '';\n\n        // Look for: /* comments */\n        if (!preg_match_all('@/\\*(.*)\\*/@msU', $content, $matches)) {\n            return $content;\n        }\n\n        foreach ($matches[1] as $macro) {\n            // Look for: =include something\n            if (!preg_match_all('/=([^\\\\s]*)\\\\s(.*)\\n/', $macro, $matches2)) {\n                continue;\n            }\n\n            foreach ($matches2[1] as $index => $macroName) {\n                $method = 'directive' . ucfirst(strtolower($macroName));\n\n                if (method_exists($this, $method)) {\n                    $imported .= $this->$method($matches2[2][$index]);\n                }\n            }\n        }\n\n        return $imported . $content;\n    }\n\n    /**\n     * Directive to process script includes\n     */\n    protected function directiveInclude(string $data, bool $required = false): string\n    {\n        $require = explode(',', $data);\n        $result = \"\";\n\n        foreach ($require as $script) {\n            $script = trim($script);\n\n            if (!File::extension($script)) {\n                $script = $script . '.js';\n            }\n\n            $scriptPath = realpath($this->scriptPath . '/' . $script);\n            if (!File::isFile($scriptPath)) {\n                $errorMsg = sprintf(\"File '%s' not found. in %s\", $script, $this->scriptFile);\n                if ($required) {\n                    throw new RuntimeException($errorMsg);\n                }\n\n                $result .= '/* ' . $errorMsg . ' */' . PHP_EOL;\n                continue;\n            }\n\n            /*\n             * Exclude duplicates\n             */\n            if (in_array($script, $this->includedFiles)) {\n                continue;\n            }\n\n            $this->includedFiles[] = $script;\n\n            /*\n             * Nested parsing\n             */\n            $oldScriptPath = $this->scriptPath;\n            $oldScriptFile = $this->scriptFile;\n\n            $this->scriptPath = dirname($scriptPath);\n            $this->scriptFile = basename($scriptPath);\n\n            $content = File::get($scriptPath);\n            $content = $this->parse($content) . PHP_EOL;\n\n            $this->scriptPath = $oldScriptPath;\n            $this->scriptFile = $oldScriptFile;\n\n            /*\n             * Parse in \"magic constants\"\n             */\n            $content = str_replace(\n                ['__DATE__', '__FILE__'],\n                [date(\"D M j G:i:s T Y\"), $script],\n                $content\n            );\n\n            $result .= $content;\n        }\n\n        return $result;\n    }\n\n    /**\n     * Directive to process mandatory script includes\n     */\n    protected function directiveRequire(string $data): string\n    {\n        return $this->directiveInclude($data, true);\n    }\n\n    /**\n     * Directive to define and replace variables\n     */\n    protected function directiveDefine(string $data): string\n    {\n        if (preg_match('@([^\\\\s]*)\\\\s+(.*)@', $data, $matches)) {\n            // str_replace($matches[1], $matches[2], $context);\n            $this->definedVars[] = [$matches[1], $matches[2]];\n        }\n\n        return '';\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Filter/LessCompiler.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Filter;\n\nuse October\\Rain\\Assetic\\Asset\\AssetInterface;\nuse October\\Rain\\Assetic\\Factory\\AssetFactory;\nuse October\\Rain\\Assetic\\Filter\\LessphpFilter;\nuse October\\Rain\\Assetic\\Filter\\HashableInterface;\nuse October\\Rain\\Assetic\\Filter\\DependencyExtractorInterface;\nuse October\\Rain\\Assetic\\Filter\\FilterInterface;\nuse Less_Parser;\n\n/**\n * Less.php Compiler Filter\n * Class used to compiled stylesheet less files, not using leafo!\n *\n * @package october/parse\n * @author Alexey Bobkov, Samuel Georges\n */\nclass LessCompiler implements FilterInterface, HashableInterface, DependencyExtractorInterface\n{\n    /**\n     * @var array presets\n     */\n    protected $presets = [];\n\n    /**\n     * @var string lastHash\n     */\n    protected $lastHash;\n\n    /**\n     * setPresets\n     */\n    public function setPresets(array $presets)\n    {\n        $this->presets = $presets;\n    }\n\n    /**\n     * filterLoad\n     */\n    public function filterLoad(AssetInterface $asset): void\n    {\n        $parser = new Less_Parser();\n\n        // CSS Rewriter will take care of this\n        $parser->SetOption('relativeUrls', false);\n\n        $parser->parseFile($asset->getSourceRoot() . '/' . $asset->getSourcePath());\n\n        // Set the LESS variables after parsing to override them\n        $parser->ModifyVars($this->presets);\n\n        $asset->setContent($parser->getCss());\n    }\n\n    /**\n     * filterDump\n     */\n    public function filterDump(AssetInterface $asset): void\n    {\n    }\n\n    /**\n     * hashAsset\n     */\n    public function hashAsset($asset, $localPath)\n    {\n        $factory = new AssetFactory($localPath);\n        $children = $this->getChildren($factory, file_get_contents($asset), dirname($asset));\n\n        $allFiles = [];\n        foreach ($children as $child) {\n            $allFiles[] = $child;\n        }\n\n        $modified = [];\n        foreach ($allFiles as $file) {\n            $modified[] = $file->getLastModified();\n        }\n\n        return md5(implode('|', $modified));\n    }\n\n    /**\n     * setHash\n     */\n    public function setHash($hash)\n    {\n        $this->lastHash = $hash;\n    }\n\n    /**\n     * hash generated for the object\n     * @return string\n     */\n    public function hash()\n    {\n        return $this->lastHash ?: serialize($this);\n    }\n\n    /**\n     * getChildren loads children recursively\n     */\n    public function getChildren(AssetFactory $factory, $content, $loadPath = null)\n    {\n        $children = (new LessphpFilter)->getChildren($factory, $content, $loadPath);\n\n        foreach ($children as $child) {\n            $childContent = file_get_contents($child->getSourceRoot().'/'.$child->getSourcePath());\n            $children = array_merge($children, (new LessphpFilter)->getChildren($factory, $childContent, $loadPath.'/'.dirname($child->getSourcePath())));\n        }\n\n        return $children;\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Filter/LessphpFilter.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Filter;\n\n\nuse October\\Rain\\Assetic\\Asset\\AssetInterface;\nuse October\\Rain\\Assetic\\Factory\\AssetFactory;\nuse October\\Rain\\Assetic\\Util\\LessUtils;\n\n/**\n * Loads LESS files using the PHP implementation of less, lessphp.\n *\n * Less files are mostly compatible, but there are slight differences.\n *\n * @link http://leafo.net/lessphp/\n *\n * @author David Buchmann <david@liip.ch>\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nclass LessphpFilter implements DependencyExtractorInterface\n{\n    private $presets = [];\n    private $formatter;\n    private $preserveComments;\n    private $customFunctions = [];\n    private $options = [];\n\n    /**\n     * Lessphp Load Paths\n     *\n     * @var array\n     */\n    protected $loadPaths = [];\n\n    /**\n     * Adds a load path to the paths used by lessphp\n     *\n     * @param string $path Load Path\n     */\n    public function addLoadPath($path)\n    {\n        $this->loadPaths[] = $path;\n    }\n\n    /**\n     * Sets load paths used by lessphp\n     *\n     * @param array $loadPaths Load paths\n     */\n    public function setLoadPaths(array $loadPaths)\n    {\n        $this->loadPaths = $loadPaths;\n    }\n\n    public function setPresets(array $presets)\n    {\n        $this->presets = $presets;\n    }\n\n    public function setOptions(array $options)\n    {\n        $this->options = $options;\n    }\n\n    /**\n     * @param string $formatter One of \"lessjs\", \"compressed\", or \"classic\".\n     */\n    public function setFormatter($formatter)\n    {\n        $this->formatter = $formatter;\n    }\n\n    /**\n     * @param boolean $preserveComments\n     */\n    public function setPreserveComments($preserveComments)\n    {\n        $this->preserveComments = $preserveComments;\n    }\n\n    public function filterLoad(AssetInterface $asset): void\n    {\n        $lc = new \\lessc();\n        if ($dir = $asset->getSourceDirectory()) {\n            $lc->importDir = $dir;\n        }\n\n        foreach ($this->loadPaths as $loadPath) {\n            $lc->addImportDir($loadPath);\n        }\n\n        foreach ($this->customFunctions as $name => $callable) {\n            $lc->registerFunction($name, $callable);\n        }\n\n        if ($this->formatter) {\n            $lc->setFormatter($this->formatter);\n        }\n\n        if (null !== $this->preserveComments) {\n            $lc->setPreserveComments($this->preserveComments);\n        }\n\n        if (method_exists($lc, 'setOptions') && count($this->options) > 0) {\n            $lc->setOptions($this->options);\n        }\n\n        $asset->setContent($lc->parse($asset->getContent(), $this->presets));\n    }\n\n    public function registerFunction($name, $callable)\n    {\n        $this->customFunctions[$name] = $callable;\n    }\n\n    public function filterDump(AssetInterface $asset): void\n    {\n    }\n\n    public function getChildren(AssetFactory $factory, $content, $loadPath = null)\n    {\n        $loadPaths = $this->loadPaths;\n        if ($loadPath !== null) {\n            $loadPaths[] = $loadPath;\n        }\n\n        if (empty($loadPaths)) {\n            return [];\n        }\n\n        $children = [];\n        foreach (LessUtils::extractImports($content) as $reference) {\n            if (substr($reference, -4) === '.css') {\n                // skip normal css imports\n                // todo: skip imports with media queries\n                continue;\n            }\n\n            if (substr($reference, -5) !== '.less') {\n                $reference .= '.less';\n            }\n\n            foreach ($loadPaths as $loadPath) {\n                if (file_exists($file = $loadPath.'/'.$reference)) {\n                    $coll = $factory->createAsset($file, [], ['root' => $loadPath]);\n                    foreach ($coll as $leaf) {\n                        $leaf->ensureFilter($this);\n                        $children[] = $leaf;\n                        break 2;\n                    }\n                }\n            }\n        }\n\n        return $children;\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Filter/ScssCompiler.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Filter;\n\nuse Event;\nuse October\\Rain\\Assetic\\Asset\\AssetInterface;\nuse October\\Rain\\Assetic\\Factory\\AssetFactory;\nuse October\\Rain\\Assetic\\Filter\\ScssphpFilter;\nuse October\\Rain\\Assetic\\Filter\\HashableInterface;\nuse October\\Rain\\Assetic\\Filter\\DependencyExtractorInterface;\nuse October\\Rain\\Assetic\\Filter\\FilterInterface;\n\n/**\n * Less.php Compiler Filter\n * Class used to compiled stylesheet less files, not using leafo!\n *\n * @package october/parse\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ScssCompiler extends ScssphpFilter implements HashableInterface, DependencyExtractorInterface\n{\n    protected $currentFiles = [];\n\n    protected $variables = [];\n\n    protected $lastHash;\n\n    public function __construct()\n    {\n        Event::listen('cms.combiner.beforePrepare', function ($compiler, $assets) {\n            foreach ($assets as $asset) {\n                if (pathinfo($asset)['extension'] == 'scss') {\n                    $this->currentFiles[] = $asset;\n                }\n            }\n        });\n    }\n\n    public function setPresets(array $presets)\n    {\n        $this->variables = array_merge($this->variables, $presets);\n    }\n\n    public function setVariables(array $variables)\n    {\n        $this->variables = array_merge($this->variables, $variables);\n    }\n\n    public function addVariable($variable)\n    {\n        $this->variables[] = $variable;\n    }\n\n    public function filterLoad(AssetInterface $asset): void\n    {\n        parent::setVariables($this->variables);\n        parent::filterLoad($asset);\n    }\n\n    public function setHash($hash)\n    {\n        $this->lastHash = $hash;\n    }\n\n    /**\n     * Generates a hash for the object\n     * @return string\n     */\n    public function hash()\n    {\n        return $this->lastHash ?: serialize($this);\n    }\n\n    public function hashAsset($asset, $localPath)\n    {\n        $factory = new AssetFactory($localPath);\n        $children = $this->getChildren($factory, file_get_contents($asset), dirname($asset));\n\n        $allFiles = [];\n        foreach ($children as $child) {\n            $allFiles[] = $child;\n        }\n\n        $modifieds = [];\n        foreach ($allFiles as $file) {\n            $modifieds[] = $file->getLastModified();\n        }\n\n        return md5(implode('|', $modifieds));\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Filter/ScssphpFilter.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Filter;\n\nuse Url;\nuse File;\nuse Config;\nuse Storage;\nuse October\\Rain\\Assetic\\Asset\\AssetInterface;\nuse October\\Rain\\Assetic\\Factory\\AssetFactory;\nuse October\\Rain\\Assetic\\Util\\SassUtils;\nuse ScssPhp\\ScssPhp\\Compiler;\n\n/**\n * ScssphpFilter loads SCSS files using the PHP implementation of scss, scssphp.\n *\n * Scss files are mostly compatible, but there are slight differences.\n *\n * @link https://github.com/scssphp/scssphp\n *\n * @author Bart van den Burg <bart@samson-it.nl>\n */\nclass ScssphpFilter implements DependencyExtractorInterface\n{\n    /**\n     * @var array importPaths\n     */\n    protected $importPaths = [];\n\n    /**\n     * @var array customFunctions\n     */\n    protected $customFunctions = [];\n\n    /**\n     * @var mixed formatter\n     */\n    protected $formatter;\n\n    /**\n     * @var array variables\n     */\n    protected $variables = [];\n\n    /**\n     * setFormatter\n     */\n    public function setFormatter($formatter)\n    {\n        $this->formatter = $formatter;\n    }\n\n    /**\n     * setVariables\n     */\n    public function setVariables(array $variables)\n    {\n        $this->variables = $variables;\n    }\n\n    /**\n     * addVariable\n     */\n    public function addVariable($variable)\n    {\n        $this->variables[] = $variable;\n    }\n\n    /**\n     * setImportPaths\n     */\n    public function setImportPaths(array $paths)\n    {\n        $this->importPaths = $paths;\n    }\n\n    /**\n     * addImportPath\n     */\n    public function addImportPath($path)\n    {\n        $this->importPaths[] = $path;\n    }\n\n    /**\n     * registerFunction\n     */\n    public function registerFunction($name, $callable)\n    {\n        $this->customFunctions[$name] = $callable;\n    }\n\n    /**\n     * filterLoad\n     */\n    public function filterLoad(AssetInterface $asset): void\n    {\n        $sc = new Compiler();\n\n        if ($dir = $asset->getSourceDirectory()) {\n            $sc->addImportPath($dir);\n        }\n\n        foreach ($this->importPaths as $path) {\n            $sc->addImportPath($path);\n        }\n\n        foreach ($this->customFunctions as $name => $callable) {\n            $sc->registerFunction($name, $callable);\n        }\n\n        if ($this->formatter) {\n            $sc->setOutputStyle($this->formatter);\n        }\n\n        if (!empty($this->variables)) {\n            $sc->addVariables($this->variables);\n        }\n\n        // Generate source map file\n        $useSourceMaps = Config::get('cms.enable_asset_source_maps', false);\n        if ($useSourceMaps) {\n            $mapFile = md5($asset->getSourcePath()).'.css.map';\n\n            $sc->setSourceMap(Compiler::SOURCE_MAP_FILE);\n            $sc->setSourceMapOptions([\n                'sourceMapURL' => $this->getSourceMapPublicUrl().'/'.$mapFile,\n                'sourceMapBasepath' => '',\n                'sourceRoot' => '/',\n            ]);\n\n            $result = $sc->compileString($asset->getContent());\n            File::put($this->getSourceMapLocalPath().'/'.$mapFile, $result->getSourceMap());\n        }\n        else {\n            $result = $sc->compileString($asset->getContent());\n        }\n\n        $asset->setContent($result->getCss());\n    }\n\n    /**\n     * filterDump\n     */\n    public function filterDump(AssetInterface $asset): void\n    {\n    }\n\n    /**\n     * getChildren\n     */\n    public function getChildren(AssetFactory $factory, $content, $loadPath = null)\n    {\n        $sc = new Compiler();\n        if ($loadPath !== null) {\n            $sc->addImportPath($loadPath);\n        }\n\n        foreach ($this->importPaths as $path) {\n            $sc->addImportPath($path);\n        }\n\n        $children = [];\n        foreach (SassUtils::extractImports($content) as $match) {\n            $file = $sc->findImport($match);\n            if ($file) {\n                $children[] = $child = $factory->createAsset($file, [], ['root' => $loadPath]);\n                $child->load();\n                $children = array_merge(\n                    $children,\n                    $this->getChildren($factory, $child->getContent(), $child->getSourceDirectory())\n                );\n            }\n        }\n\n        return $children;\n    }\n\n    /**\n     * getSourceMapLocalPath returns the local path for source maps\n     */\n    protected function getSourceMapLocalPath(): string\n    {\n        $path = rtrim(Config::get('filesystems.disks.resources.root', storage_path('app/resources')), '/');\n        $path .= '/sourcemap';\n\n        if (!File::isDirectory($path)) {\n            File::makeDirectory($path, 0755, true, true);\n        }\n\n        return $path;\n    }\n\n    /**\n     * getSourceMapPublicUrl returns the public address for the source map path\n     */\n    protected function getSourceMapPublicUrl(): string\n    {\n        $fullPath = Config::get('filesystems.disks.resources.url', '/storage/app/resources');\n        $fullPath .= '/sourcemap';\n\n        if (\n            Config::get('filesystems.disks.resources.driver') === 'local' &&\n            Config::get('system.relative_links') === true\n        ) {\n            return Url::toRelative($fullPath);\n        }\n\n        return Url::asset($fullPath);\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Filter/StylesheetMinify.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Filter;\n\nuse October\\Rain\\Assetic\\Asset\\AssetInterface;\nuse October\\Rain\\Assetic\\Filter\\FilterInterface;\n\n/**\n * StylesheetMinify CSS Filter\n * Class used to compress stylesheet css files.\n *\n * @package october/parse\n * @author Alexey Bobkov, Samuel Georges\n */\nclass StylesheetMinify implements FilterInterface\n{\n    /**\n     * filterLoad\n     */\n    public function filterLoad(AssetInterface $asset): void\n    {\n    }\n\n    /**\n     * filterDump\n     */\n    public function filterDump(AssetInterface $asset): void\n    {\n        $asset->setContent($this->minify($asset->getContent()));\n    }\n\n    /**\n     * Minify CSS\n     * @param string $css CSS code to minify.\n     * @return string Minified CSS.\n     */\n    protected function minify(string $css): string\n    {\n        // Normalize whitespace in a smart way\n        $css = preg_replace('/\\s{2,}/', ' ', $css);\n\n        // Remove spaces before and after comment\n        $css = preg_replace('/(\\s+)(\\/\\*[^!](.*?)\\*\\/)(\\s+)/', '$2', $css);\n\n        // Add a space after retained /*! comments to prevent valid CSS from appearing as a\n        // removable comment, eg: /*! keepme */*,:after{content:'nuked'}/*another comment*/\n        $css = preg_replace('#(/\\*!.*?\\*/)(\\*)#s', '$1 $2', $css);\n\n        // Remove comment blocks, everything between /* and */, ignore /*! comments\n        $css = preg_replace('#/\\*[^\\!].*?\\*/#s', '', $css);\n\n        // Remove ; before }\n        $css = preg_replace('/;(?=\\s*})/', '', $css);\n\n        // Remove space after , : ; { } */ >, but not after !*/\n        $css = preg_replace('/(,|:|;|\\{|}|[^!]\\*\\/|>) /', '$1', $css);\n\n        // Remove space before , ; { } >\n        $css = preg_replace('/ (,|;|\\{|}|>)/', '$1', $css);\n\n        // Remove newline before } >\n        $css = preg_replace('/(\\r\\n|\\r|\\n)(})/', '$2', $css);\n\n        // Remove trailing zeros from float numbers preceded by : or a white-space\n        // -6.0100em to -6.01em, .0100 to .01, 1.200px to 1.2px\n        $css = preg_replace('/((?<!\\\\\\\\)\\:|\\s)(\\-?)(\\d?\\.\\d+?)0+([^\\d])/S', '$1$2$3$4', $css);\n\n        // Shorten 6-character hex color codes to 3-character where possible\n        $css = preg_replace('/#([a-f0-9])\\\\1([a-f0-9])\\\\2([a-f0-9])\\\\3/i', '#\\1\\2\\3', $css);\n\n        return trim($css);\n    }\n}\n"
  },
  {
    "path": "src/Assetic/FilterManager.php",
    "content": "<?php namespace October\\Rain\\Assetic;\n\nuse October\\Rain\\Assetic\\Filter\\FilterInterface;\nuse InvalidArgumentException;\n\n/**\n * FilterManager manages the available filters.\n *\n * @package october/assetic\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nclass FilterManager\n{\n    /**\n     * @var array filters\n     */\n    protected $filters = [];\n\n    /**\n     * set\n     */\n    public function set(string $alias, FilterInterface $filter): void\n    {\n        $this->checkName($alias);\n\n        $this->filters[$alias] = $filter;\n    }\n\n    /**\n     * get\n     */\n    public function get(string $alias): FilterInterface\n    {\n        if (!isset($this->filters[$alias])) {\n            throw new InvalidArgumentException(sprintf('There is no \"%s\" filter.', $alias));\n        }\n\n        return $this->filters[$alias];\n    }\n\n    /**\n     * has\n     */\n    public function has(string $alias): bool\n    {\n        return isset($this->filters[$alias]);\n    }\n\n    /**\n     * getNames\n     */\n    public function getNames(): array\n    {\n        return array_keys($this->filters);\n    }\n\n    /**\n     * Checks that a name is valid.\n     * @param string $name An asset name candidate\n     * @throws InvalidArgumentException If the asset name is invalid\n     */\n    protected function checkName(string $name): void\n    {\n        if (!ctype_alnum(str_replace('_', '', $name))) {\n            throw new InvalidArgumentException(sprintf('The name \"%s\" is invalid.', $name));\n        }\n    }\n}\n"
  },
  {
    "path": "src/Assetic/README.md",
    "content": "# Rain Assetic Resources\n\nAssetic is a simple library that lets you compile and combine basic LESS and SCSS files.\n\n## Basic usage\n\nYou may use the `parse` methods to parse LESS or SCSS respectively, the first argument is the asset paths and the second argument is the options. The file extension determines which compiler is used, either `.less` or `.scss`.\n\n```php\n$combiner = new October\\Rain\\Assetic\\Combiner;\n\necho $combiner->parse([\n    '/path/to/src/styles.less',\n    '/path/to/src/theme.less'\n], [\n    'production' => true\n]);\n```\n\nThe following options are supported\n\nOptions | Usage\n------- | ---------\n`production` | Combine with production filters (eg: minification).\n`targetPath` | Sets the target output path for rewriting asset locations.\n`useCache` | Use a file based cache to speed up performance.\n`deepHashKey` | Cache key used for busting deep hashing.\n"
  },
  {
    "path": "src/Assetic/Traits/HasDeepHasher.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Traits;\n\nuse File;\nuse October\\Rain\\Assetic\\Factory\\AssetFactory;\n\n/**\n * HasDeepHasher checks if imports have changed their content and busts the cache\n *\n * @package october/assetic\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasDeepHasher\n{\n    /**\n     * getDeepHashFromCombiner\n     */\n    public function getDeepHashLastModified($combiner)\n    {\n        $factory = new AssetFactory($this->localPath);\n        return $factory->getLastModified($combiner);\n    }\n\n    /**\n     * getDeepHashFromAssets returns a deep hash on filters that support it.\n     * @param array $assets List of asset files.\n     * @return string\n     */\n    public function getDeepHashFromAssets($assets)\n    {\n        $key = '';\n\n        $assetFiles = [];\n        foreach ($assets as $file) {\n            $path = File::symbolizePath($file);\n            if (file_exists($path)) {\n                $assetFiles[] = $path;\n            }\n            elseif (file_exists($this->localPath . $path)) {\n                $assetFiles[] = $this->localPath . $path;\n            }\n        }\n\n        foreach ($assetFiles as $file) {\n            $filters = $this->getFilters(File::extension($file));\n\n            foreach ($filters as $filter) {\n                if (method_exists($filter, 'hashAsset')) {\n                    $key .= $filter->hashAsset($file, $this->localPath);\n                }\n            }\n        }\n\n        return $key;\n    }\n\n    /**\n     * setHashOnCombinerFilters busts the cache based on a different cache key.\n     */\n    protected function setDeepHashKeyOnFilters($hash)\n    {\n        $allFilters = array_merge(...array_values($this->getFilters()));\n\n        foreach ($allFilters as $filter) {\n            if (method_exists($filter, 'setHash')) {\n                $filter->setHash($hash);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Util/CssUtils.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Util;\n\n/**\n * CSS Utils.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nabstract class CssUtils\n{\n    const REGEX_URLS            = '/url\\(([\"\\']?)(?P<url>.*?)(\\\\1)\\)/';\n    const REGEX_IMPORTS         = '/@import (?:url\\()?(\\'|\"|)(?P<url>[^\\'\"\\)\\n\\r]*)\\1\\)?;?/';\n    const REGEX_IMPORTS_NO_URLS = '/@import (?!url\\()(\\'|\"|)(?P<url>[^\\'\"\\)\\n\\r]*)\\1;?/';\n    const REGEX_IE_FILTERS      = '/src=([\"\\']?)(?P<url>.*?)\\\\1/';\n    const REGEX_COMMENTS        = '/(\\/\\*[^*]*\\*+(?:[^\\/][^*]*\\*+)*\\/)/';\n\n    /**\n     * Filters all references -- url() and \"@import\" -- through a callable.\n     *\n     * @param string   $content  The CSS\n     * @param callable $callback A PHP callable\n     *\n     * @return string The filtered CSS\n     */\n    public static function filterReferences(string $content, callable $callback): string\n    {\n        $content = static::filterUrls($content, $callback);\n        $content = static::filterImports($content, $callback, false);\n        $content = static::filterIEFilters($content, $callback);\n\n        return $content;\n    }\n\n    /**\n     * Filters all CSS url()'s through a callable.\n     *\n     * @param string   $content  The CSS\n     * @param callable $callback A PHP callable\n     *\n     * @return string The filtered CSS\n     */\n    public static function filterUrls(string $content, callable $callback): string\n    {\n        $pattern = static::REGEX_URLS;\n\n        return static::filterCommentless($content, function ($part) use (&$callback, $pattern) {\n            return preg_replace_callback($pattern, $callback, $part);\n        });\n    }\n\n    /**\n     * Filters all CSS imports through a callable.\n     *\n     * @param string   $content    The CSS\n     * @param callable $callback   A PHP callable\n     * @param Boolean  $includeUrl Whether to include url() in the pattern\n     *\n     * @return string The filtered CSS\n     */\n    public static function filterImports(string $content, callable $callback, bool $includeUrl = true): string\n    {\n        $pattern = $includeUrl ? static::REGEX_IMPORTS : static::REGEX_IMPORTS_NO_URLS;\n\n        return static::filterCommentless($content, function ($part) use (&$callback, $pattern) {\n            return preg_replace_callback($pattern, $callback, $part);\n        });\n    }\n\n    /**\n     * Filters all IE filters (AlphaImageLoader filter) through a callable.\n     *\n     * @param string   $content  The CSS\n     * @param callable $callback A PHP callable\n     *\n     * @return string The filtered CSS\n     */\n    public static function filterIEFilters(string $content, callable $callback): string\n    {\n        $pattern = static::REGEX_IE_FILTERS;\n\n        return static::filterCommentless($content, function ($part) use (&$callback, $pattern) {\n            return preg_replace_callback($pattern, $callback, $part);\n        });\n    }\n\n    /**\n     * Filters each non-comment part through a callable.\n     *\n     * @param string   $content  The CSS\n     * @param callable $callback A PHP callable\n     *\n     * @return string The filtered CSS\n     */\n    public static function filterCommentless(string $content, callable $callback): string\n    {\n        $result = '';\n        foreach (preg_split(static::REGEX_COMMENTS, $content, -1, PREG_SPLIT_DELIM_CAPTURE) as $part) {\n            if (!preg_match(static::REGEX_COMMENTS, $part, $match) || $part != $match[0]) {\n                $part = $callback($part);\n            }\n\n            $result .= $part;\n        }\n\n        return $result;\n    }\n\n    /**\n     * Extracts all references from the supplied CSS content.\n     *\n     * @param string $content The CSS content\n     *\n     * @return array An array of unique URLs\n     */\n    public static function extractImports(string $content): array\n    {\n        $imports = [];\n        static::filterImports($content, function ($matches) use (&$imports) {\n            $imports[] = $matches['url'];\n        });\n\n        return array_unique(array_filter($imports));\n    }\n\n    final private function __construct()\n    {\n    }\n}\n"
  },
  {
    "path": "src/Assetic/Util/LessUtils.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Util;\n\n\n/**\n * Less Utils.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nabstract class LessUtils extends CssUtils\n{\n    const REGEX_IMPORTS         = '/@import(?:-once)? (?:\\([a-z]*\\) )?(?:url\\()?(\\'|\"|)(?P<url>[^\\'\"\\)\\n\\r]*)\\1\\)?;?/';\n    const REGEX_IMPORTS_NO_URLS = '/@import(?:-once)? (?:\\([a-z]*\\) )?(?!url\\()(\\'|\"|)(?P<url>[^\\'\"\\)\\n\\r]*)\\1;?/';\n    const REGEX_COMMENTS        = '/((?:\\/\\*[^*]*\\*+(?:[^\\/][^*]*\\*+)*\\/)|\\/\\/[^\\n]+)/';\n}\n"
  },
  {
    "path": "src/Assetic/Util/SassUtils.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Util;\n\n/*\n * This file is part of the Assetic package, an OpenSky project.\n *\n * (c) 2010-2015 OpenSky Project Inc\n *\n * For the full copyright and license information, please view the LICENSE\n * file that was distributed with this source code.\n */\n\n/**\n * Sass Utils.\n *\n * @author Kris Wallsmith <kris.wallsmith@gmail.com>\n */\nabstract class SassUtils extends CssUtils\n{\n    const REGEX_COMMENTS = '/((?:\\/\\*[^*]*\\*+(?:[^\\/][^*]*\\*+)*\\/)|\\/\\/[^\\n]+)/';\n}\n"
  },
  {
    "path": "src/Assetic/Util/VarUtils.php",
    "content": "<?php namespace October\\Rain\\Assetic\\Util;\n\n/**\n * Variable utilities.\n *\n * @author Johannes M. Schmitt <schmittjoh@gmail.com>\n */\nabstract class VarUtils\n{\n    /**\n     * Resolves variable placeholders.\n     *\n     * @param string $template A template string\n     * @param array  $vars     Variable names\n     * @param array  $values   Variable values\n     *\n     * @return string The resolved string\n     *\n     * @throws \\InvalidArgumentException If there is a variable with no value\n     */\n    public static function resolve(string $template, array $vars, array $values): string\n    {\n        $map = [];\n        foreach ($vars as $var) {\n            if (false === strpos($template, '{'.$var.'}')) {\n                continue;\n            }\n\n            if (!isset($values[$var])) {\n                throw new \\InvalidArgumentException(sprintf('The template \"%s\" contains the variable \"%s\", but was not given any value for it.', $template, $var));\n            }\n\n            $map['{'.$var.'}'] = $values[$var];\n        }\n\n        return strtr($template, $map);\n    }\n\n    public static function getCombinations(array $vars, array $values): array\n    {\n        if (!$vars) {\n            return [[]];\n        }\n\n        $combinations = [];\n        $nbValues = [];\n        foreach ($values as $var => $vals) {\n            if (!in_array($var, $vars, true)) {\n                continue;\n            }\n\n            $nbValues[$var] = count($vals);\n        }\n\n        for ($i = array_product($nbValues), $c = $i * 2; $i < $c; $i++) {\n            $k = $i;\n            $combination = [];\n\n            foreach ($vars as $var) {\n                $combination[$var] = $values[$var][$k % $nbValues[$var]];\n                $k = intval($k / $nbValues[$var]);\n            }\n\n            $combinations[] = $combination;\n        }\n\n        return $combinations;\n    }\n\n    final private function __construct()\n    {\n    }\n}\n"
  },
  {
    "path": "src/Auth/AuthException.php",
    "content": "<?php namespace October\\Rain\\Auth;\n\nuse Config;\nuse October\\Rain\\Exception\\ApplicationException;\nuse Exception;\n\n/**\n * AuthException used when user authentication fails. Implements a softer error message\n *\n * @package october\\auth\n * @author Alexey Bobkov, Samuel Georges\n */\nclass AuthException extends ApplicationException\n{\n    /**\n     * @var string errorMessage default soft error message\n     */\n    protected static $errorMessage = 'The details you entered did not match our records. Please double-check and try again.';\n\n    /**\n     * @var array errorCodes for each error distinction.\n     */\n    protected static $errorCodes = [\n        // Input errors\n        100 => 'Missing Attribute',\n        101 => 'Missing Login Attribute',\n        102 => 'Missing Password Attribute',\n\n        // Lookup errors\n        200 => 'User Not Found',\n        201 => 'Wrong Password',\n\n        // State errors\n        300 => 'User Not Activated',\n        301 => 'User Suspended',\n        302 => 'User Banned',\n\n        // Context errors\n        400 => 'User Not Logged In',\n        401 => 'User Forbidden',\n    ];\n\n    /**\n     * __construct softens a detailed authentication error with a more vague message when\n     * the application is not in debug mode for security reasons.\n     * @param string $message\n     * @param int $code\n     * @param Exception $previous\n     */\n    public function __construct($message = '', $code = 0, ?Exception $previous = null)\n    {\n        if ($this->useSoftErrors()) {\n            $message = static::$errorMessage;\n        }\n\n        if (isset(static::$errorCodes[$code])) {\n            $this->errorType = static::$errorCodes[$code];\n        }\n\n        parent::__construct(__($message), $code, $previous);\n    }\n\n    /**\n     * setDefaultErrorMessage will override the soft error message displayed to the user\n     */\n    public static function setDefaultErrorMessage(string $message)\n    {\n        static::$errorMessage = $message;\n    }\n\n    /**\n     * useSoftErrors determines if soft errors should be used, set by config and when\n     * enabled uses less specific error messages.\n     */\n    protected function useSoftErrors(): bool\n    {\n        if (Config::get('system.soft_auth_errors') !== null) {\n            return (bool) Config::get('system.soft_auth_errors');\n        }\n\n        return !Config::get('app.debug', false);\n    }\n}\n"
  },
  {
    "path": "src/Auth/Concerns/HasGuard.php",
    "content": "<?php namespace October\\Rain\\Auth\\Concerns;\n\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\n\n/**\n * HasGuard defines all methods to satisfy the Laravel contract\n *\n * @package october\\auth\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasGuard\n{\n    /**\n     * check to see if the user is logged in and activated, and hasn't been banned or suspended.\n     * @return bool\n     */\n    public function check()\n    {\n        if ($this->checkCache !== null) {\n            return $this->checkCache;\n        }\n\n        if (is_null($this->user)) {\n            // Find persistence code\n            $userArray = $this->getPersistCodeFromSession();\n            if (!$userArray) {\n                return false;\n            }\n\n            [$id, $persistCode] = $userArray;\n\n            // Look up user\n            if (!$user = $this->findUserById($id)) {\n                return $this->checkCache = false;\n            }\n\n            // Confirm the persistence code is valid, otherwise reject\n            if (!$user->checkPersistCode($persistCode)) {\n                return $this->checkCache = false;\n            }\n\n            // Pass\n            $this->user = $user;\n        }\n\n        // Check cached user is activated\n        if (!($user = $this->getUser()) || ($this->requireActivation && !$user->is_activated)) {\n            return false;\n        }\n\n        // Throttle check\n        if ($this->useThrottle) {\n            $throttle = $this->findThrottleByUserId($user->getKey(), $this->ipAddress);\n\n            if ($throttle->is_banned || $throttle->checkSuspended()) {\n                $this->logout();\n                return false;\n            }\n        }\n\n        // Role impersonation\n        if ($this->isRoleImpersonator()) {\n            $this->applyRoleImpersonation($this->user);\n        }\n\n        return true;\n    }\n\n    /**\n     * guest determines if the current user is a guest.\n     * @return bool\n     */\n    public function guest()\n    {\n        return false;\n    }\n\n    /**\n     * user will return the currently authenticated user.\n     * @return \\Illuminate\\Contracts\\Auth\\Authenticatable|null\n     */\n    public function user()\n    {\n        return $this->getUser();\n    }\n\n    /**\n     * id for the currently authenticated user.\n     * @return int|null\n     */\n    public function id()\n    {\n        if ($user = $this->getUser()) {\n            return $user->getAuthIdentifier();\n        }\n\n        return null;\n    }\n\n    /**\n     * validate a user's credentials.\n     * @return bool\n     */\n    public function validate(array $credentials = [])\n    {\n        return !!$this->validateInternal($credentials);\n    }\n\n    /**\n     * hasUser determines if the guard has a user instance.\n     * @return bool\n     */\n    public function hasUser()\n    {\n        return !is_null($this->user);\n    }\n\n    /**\n     * setUser will set the current user.\n     */\n    public function setUser(Authenticatable $user)\n    {\n        $this->user = $user;\n    }\n}\n"
  },
  {
    "path": "src/Auth/Concerns/HasImpersonation.php",
    "content": "<?php namespace October\\Rain\\Auth\\Concerns;\n\nuse Session;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\n\n/**\n * HasImpersonation\n *\n * @package october\\auth\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasImpersonation\n{\n    /**\n     * impersonate the given user and sets properties in the session but not the cookie.\n     */\n    public function impersonate($user)\n    {\n        // Determine previous user\n        $userArray = $this->getPersistCodeFromSession(false);\n        $oldUserId = $userArray ? $userArray[0] : null;\n\n        /**\n         * @event model.auth.beforeImpersonate\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.auth.beforeImpersonate', function (\\October\\Rain\\Database\\Model|null $oldUser) use (\\October\\Rain\\Database\\Model $model) {\n         *         traceLog($oldUser->full_name . ' is now impersonating ' . $model->full_name);\n         *     });\n         *\n         */\n        $oldUser = $oldUserId ? $this->findUserById($oldUserId) : null;\n        $user->fireEvent('model.auth.beforeImpersonate', [$oldUser]);\n\n        // Replace session with impersonated user\n        $this->setPersistCodeToSession($user, false, true);\n\n        // If this is the first time impersonating, capture the original user\n        if (!$this->isImpersonator()) {\n            Session::put($this->sessionKey.'_impersonate', $oldUserId ?: 'NaN');\n        }\n    }\n\n    /**\n     * stopImpersonate stops the current session being impersonated and\n     * attempts to authenticate as the impersonator again.\n     */\n    public function stopImpersonate()\n    {\n        // Determine current and previous user\n        $userArray = $this->getPersistCodeFromSession(false);\n        $currentUserId = $userArray ? $userArray[0] : null;\n        $oldUser = $this->getImpersonator();\n\n        if ($currentUserId && ($currentUser = $this->findUserById($currentUserId))) {\n            /**\n             * @event model.auth.afterImpersonate\n             *\n             * Example usage:\n             *\n             *     $model->bindEvent('model.auth.afterImpersonate', function (\\October\\Rain\\Database\\Model|null $oldUser) use (\\October\\Rain\\Database\\Model $model) {\n             *         traceLog($oldUser->full_name . ' has stopped impersonating ' . $model->full_name);\n             *     });\n             *\n             */\n            $currentUser->fireEvent('model.auth.afterImpersonate', [$oldUser]);\n        }\n\n        // Restore previous user, if possible\n        if ($oldUser) {\n            $this->setPersistCodeToSession($oldUser, false, true);\n        }\n        else {\n            Session::forget($this->sessionKey);\n        }\n\n        Session::forget($this->sessionKey.'_impersonate');\n    }\n\n    /**\n     * getRealUser gets the \"real\" user to bypass impersonation.\n     * @return Authenticatable|null\n     */\n    public function getRealUser()\n    {\n        if ($user = $this->getImpersonator()) {\n            return $user;\n        }\n\n        return $this->getUser();\n    }\n\n    /**\n     * isImpersonator checks to see if the current session is being impersonated.\n     * @return bool\n     */\n    public function isImpersonator()\n    {\n        return Session::has($this->sessionKey.'_impersonate');\n    }\n\n    /**\n     * getImpersonator gets the original user doing the impersonation\n     * @return \\Illuminate\\Contracts\\Auth\\Authenticatable|null\n     */\n    public function getImpersonator()\n    {\n        if (!Session::has($this->sessionKey.'_impersonate')) {\n            return null;\n        }\n\n        $oldUserId = Session::get($this->sessionKey.'_impersonate');\n        if ((!is_string($oldUserId) && !is_int($oldUserId)) || $oldUserId === 'NaN') {\n            return null;\n        }\n\n        return $this->createUserModel()->find($oldUserId);\n    }\n\n    /**\n     * impersonateRole will impersonate a role for the current user\n     */\n    public function impersonateRole($role)\n    {\n        Session::put($this->sessionKey.'_impersonate_role', $role->getKey());\n    }\n\n    /**\n     * isRoleImpersonator\n     */\n    public function isRoleImpersonator(): bool\n    {\n        return !empty(Session::has($this->sessionKey.'_impersonate_role'));\n    }\n\n    /**\n     * stopImpersonateRole will stop role impersonation\n     */\n    public function stopImpersonateRole()\n    {\n        Session::forget($this->sessionKey.'_impersonate_role');\n    }\n\n    /**\n     * applyRoleImpersonation tells the user model to impersonate the role\n     */\n    protected function applyRoleImpersonation($user)\n    {\n        $roleId = Session::get($this->sessionKey.'_impersonate_role');\n\n        if ($role = $this->createRoleModel()->find($roleId)) {\n            $user->setRoleImpersonation($role);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Auth/Concerns/HasProviderProxy.php",
    "content": "<?php namespace October\\Rain\\Auth\\Concerns;\n\n/**\n * HasProviderProxy provides proxy methods to emulate Laravel's auth provider\n *\n * @package october\\auth\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasProviderProxy\n{\n    /**\n     * getProvider just passes it back to the current class\n     */\n    public function getProvider()\n    {\n        return $this;\n    }\n\n    /**\n     * getModel returns the class name for the user model\n     */\n    public function getModel()\n    {\n        return $this->userModel;\n    }\n}\n"
  },
  {
    "path": "src/Auth/Concerns/HasSession.php",
    "content": "<?php namespace October\\Rain\\Auth\\Concerns;\n\nuse Cookie;\nuse Session;\n\n/**\n * HasSession\n *\n * @package october\\auth\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasSession\n{\n    /**\n     * setPersistCodeToSession stores the user persistence in the session and cookie.\n     */\n    protected function setPersistCodeToSession($user, bool $remember = true, bool $impersonating = false)\n    {\n        $persistCode = $impersonating && $user->persist_code\n            ? $user->persist_code\n            : $user->getPersistCode();\n\n        $toPersist = [$user->getKey(), $persistCode];\n\n        Session::put($this->sessionKey, $toPersist);\n\n        if ($remember) {\n            Cookie::queue(Cookie::forever($this->sessionKey, json_encode($toPersist)));\n        }\n    }\n\n    /**\n     * getPersistCodeFromSession will return the user ID and persist token from the session.\n     * The resulting array will contain the user ID and persistence code [id, code] or null.\n     */\n    protected function getPersistCodeFromSession(bool $isChecking = true): ?array\n    {\n        // Check session first, followed by cookie\n        if ($sessionArray = Session::get($this->sessionKey)) {\n            $userArray = $sessionArray;\n        }\n        elseif ($cookieArray = Cookie::get($this->sessionKey)) {\n            if ($isChecking) {\n                $this->viaRemember = true;\n            }\n            $userArray = @json_decode($cookieArray, true);\n        }\n        else {\n            return null;\n        }\n\n        // Check supplied session/cookie is an array (user id, persist code)\n        if (!is_array($userArray) || count($userArray) !== 2) {\n            return null;\n        }\n\n        return $userArray;\n    }\n}\n"
  },
  {
    "path": "src/Auth/Concerns/HasStatefulGuard.php",
    "content": "<?php namespace October\\Rain\\Auth\\Concerns;\n\nuse Cookie;\nuse Session;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse October\\Rain\\Auth\\AuthException;\n\n/**\n * HasStatefulGuard defines all methods to satisfy the Laravel contract\n *\n * @package october\\auth\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasStatefulGuard\n{\n    /**\n     * attempt to authenticate a user using the given credentials.\n     *\n     * @param array $credentials The user login details\n     * @param bool $remember Store a non-expire cookie for the user\n     * @throws AuthException If authentication fails\n     * @return Models\\User The successfully logged in user\n     */\n    public function attempt(array $credentials = [], $remember = false)\n    {\n        return !!$this->authenticate($credentials, $remember);\n    }\n\n    /**\n     * once logs a user into the application without sessions or cookies.\n     * @param  array  $credentials\n     * @return bool\n     */\n    public function once(array $credentials = [])\n    {\n        $this->useSession = false;\n\n        $user = $this->authenticate($credentials);\n\n        $this->useSession = true;\n\n        return !!$user;\n    }\n\n    /**\n     * login the given user and sets properties in the session.\n     * @throws AuthException If the user is not activated and $this->requireActivation = true\n     */\n    public function login(Authenticatable $user, $remember = true)\n    {\n        // Fire the 'beforeLogin' event\n        $user->beforeLogin();\n\n        // Activation is required, user not activated\n        if ($this->requireActivation && !$user->is_activated) {\n            throw new AuthException('Cannot login user since they are not activated.', 300);\n        }\n\n        $this->user = $user;\n\n        // Create session/cookie data to persist the session\n        if ($this->useSession) {\n            $this->setPersistCodeToSession($user, $remember);\n        }\n\n        // Fire the 'afterLogin' event\n        $user->afterLogin();\n    }\n\n    /**\n     * loginUsingId logs the given user ID into the application.\n     * @param  mixed  $id\n     * @param  bool   $remember\n     * @return \\Illuminate\\Contracts\\Auth\\Authenticatable|bool\n     */\n    public function loginUsingId($id, $remember = false)\n    {\n        if (!is_null($user = $this->findUserById($id))) {\n            $this->login($user, $remember);\n\n            return $user;\n        }\n\n        return false;\n    }\n\n    /**\n     * onceUsingId logs the given user ID into the application without sessions or cookies.\n     * @param  mixed  $id\n     * @return \\Illuminate\\Contracts\\Auth\\Authenticatable|false\n     */\n    public function onceUsingId($id)\n    {\n        if (!is_null($user = $this->findUserById($id))) {\n            $this->setUser($user);\n\n            return $user;\n        }\n\n        return false;\n    }\n\n    /**\n     * viaRemember determines if the user was authenticated via \"remember me\" cookie.\n     * @return bool\n     */\n    public function viaRemember()\n    {\n        return $this->viaRemember;\n    }\n\n    /**\n     * logout logs the current user out.\n     */\n    public function logout()\n    {\n        // Initialize the current auth session before trying to remove it\n        if (is_null($this->user) && !$this->check()) {\n            return;\n        }\n\n        if ($this->isImpersonator()) {\n            $this->user = $this->getImpersonator();\n            $this->stopImpersonate();\n            return;\n        }\n\n        if ($this->user) {\n            $this->user->setRememberToken(null);\n            $this->user->forceSave();\n        }\n\n        $this->user = null;\n\n        Session::invalidate();\n        Cookie::queue(Cookie::forget($this->sessionKey));\n    }\n}\n"
  },
  {
    "path": "src/Auth/Concerns/HasThrottle.php",
    "content": "<?php namespace October\\Rain\\Auth\\Concerns;\n\nuse October\\Rain\\Auth\\AuthException;\n\n/**\n * HasThrottle\n *\n * @package october\\auth\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasThrottle\n{\n    /**\n     * createThrottleModel creates an instance of the throttle model.\n     * @return Models\\Throttle\n     */\n    public function createThrottleModel()\n    {\n        $class = '\\\\'.ltrim($this->throttleModel, '\\\\');\n        return new $class();\n    }\n\n    /**\n     * findThrottleByLogin and ip address\n     *\n     * @param string $loginName\n     * @param string $ipAddress\n     * @return Models\\Throttle\n     */\n    public function findThrottleByLogin($loginName, $ipAddress)\n    {\n        $user = $this->findUserByLogin($loginName);\n        if (!$user) {\n            throw new AuthException('A user was not found with the given credentials.', 200);\n        }\n\n        $userId = $user->getKey();\n        return $this->findThrottleByUserId($userId, $ipAddress);\n    }\n\n    /**\n     * findThrottleByUserId and ip address\n     *\n     * @param integer $userId\n     * @param string $ipAddress\n     * @return Models\\Throttle\n     */\n    public function findThrottleByUserId($userId, $ipAddress = null)\n    {\n        $cacheKey = md5($userId.$ipAddress);\n        if (isset($this->throttle[$cacheKey])) {\n            return $this->throttle[$cacheKey];\n        }\n\n        $model = $this->createThrottleModel();\n        $query = $model->where('user_id', '=', $userId);\n\n        if ($ipAddress) {\n            $query->where(function ($query) use ($ipAddress) {\n                $query->where('ip_address', '=', $ipAddress);\n                $query->orWhere('ip_address', '=', null);\n            });\n        }\n\n        if (!$throttle = $query->first()) {\n            $throttle = $this->createThrottleModel();\n            $throttle->user_id = $userId;\n            if ($ipAddress) {\n                $throttle->ip_address = $ipAddress;\n            }\n\n            $throttle->save();\n        }\n\n        return $this->throttle[$cacheKey] = $throttle;\n    }\n\n    /**\n     * clearThrottleForUserId unsuspends and clears all throttles records for a user\n     */\n    public function clearThrottleForUserId($userId)\n    {\n        if (!$userId) {\n            return;\n        }\n\n        $model = $this->createThrottleModel();\n\n        $throttles = $model->where('user_id', $userId)->get();\n\n        foreach ($throttles as $throttle) {\n            $throttle->unsuspend();\n        }\n    }\n}\n"
  },
  {
    "path": "src/Auth/Concerns/HasUser.php",
    "content": "<?php namespace October\\Rain\\Auth\\Concerns;\n\nuse Cookie;\nuse Session;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse October\\Rain\\Auth\\AuthException;\n\n/**\n * HasUser\n *\n * @package october\\auth\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasUser\n{\n    /**\n     * createUserModel instance\n     */\n    public function createUserModel()\n    {\n        $class = '\\\\'.ltrim($this->userModel, '\\\\');\n        return new $class();\n    }\n\n    /**\n     * createRoleModel creates an instance of the role model.\n     * @return Models\\Role\n     */\n    public function createRoleModel()\n    {\n        $class = '\\\\'.ltrim($this->roleModel, '\\\\');\n        return new $class();\n    }\n\n    /**\n     * createUserModelQuery prepares a query derived from the user model.\n     * @return \\October\\Rain\\Database\\Builder $query\n     */\n    protected function createUserModelQuery()\n    {\n        $model = $this->createUserModel();\n        $query = $model->newQuery();\n        $this->extendUserQuery($query);\n\n        return $query;\n    }\n\n    /**\n     * extendUserQuery used for finding the user.\n     * @param \\October\\Rain\\Database\\Builder $query\n     */\n    public function extendUserQuery($query)\n    {\n    }\n\n    /**\n     * hasSession returns true if a user session exists without verifying it.\n     */\n    public function hasSession(): bool\n    {\n        return Session::has($this->sessionKey);\n    }\n\n    /**\n     * hasRemember returns true if the user requested to stay logged in.\n     */\n    public function hasRemember(): bool\n    {\n        return Cookie::has($this->sessionKey);\n    }\n\n    /**\n     * getUser returns the current user, if any.\n     * @return Authenticatable|null\n     */\n    public function getUser()\n    {\n        if (is_null($this->user)) {\n            $this->check();\n        }\n\n        return $this->user;\n    }\n\n    /**\n     * findUserById finds a user by the login value.\n     * @param string $id\n     * @return Authenticatable|null\n     */\n    public function findUserById($id)\n    {\n        $query = $this->createUserModelQuery();\n\n        $user = $query->find($id);\n\n        return $this->validateUserModel($user) ? $user : null;\n    }\n\n    /**\n     * findUserByLogin finds a user by the login value.\n     * @param string $login\n     * @return Authenticatable|null\n     */\n    public function findUserByLogin($login)\n    {\n        $model = $this->createUserModel();\n\n        $query = $this->createUserModelQuery();\n\n        $user = $query->where($model->getLoginName(), $login)->first();\n\n        return $this->validateUserModel($user) ? $user : null;\n    }\n\n    /**\n     * findUserByCredentials finds a user by the given credentials.\n     * @param array $credentials\n     * @throws AuthException\n     * @return Models\\User\n     */\n    public function findUserByCredentials(array $credentials)\n    {\n        $model = $this->createUserModel();\n        $loginName = $model->getLoginName();\n\n        if (!array_key_exists($loginName, $credentials)) {\n            throw new AuthException(\"The {$loginName} attribute is required.\", 101);\n        }\n\n        $query = $this->createUserModelQuery();\n        $hashableAttributes = $model->getHashableAttributes();\n        $hashedCredentials = [];\n\n        /*\n         * Build query from given credentials\n         */\n        foreach ($credentials as $credential => $value) {\n            // All excepted the hashed attributes\n            if (in_array($credential, $hashableAttributes)) {\n                $hashedCredentials = array_merge($hashedCredentials, [$credential => $value]);\n            }\n            else {\n                $query = $query->where($credential, '=', $value);\n            }\n        }\n\n        $user = $query->first();\n        if (!$this->validateUserModel($user)) {\n            throw new AuthException('A user was not found with the given credentials.', 200);\n        }\n\n        /*\n         * Check the hashed credentials match\n         */\n        foreach ($hashedCredentials as $credential => $value) {\n            if (!$user->checkHashValue($credential, $value)) {\n                // Incorrect password\n                if ($credential === 'password') {\n                    throw new AuthException('A user was found but the password did not match.', 201);\n                }\n\n                // User not found\n                throw new AuthException('A user was not found with the given credentials.', 200);\n            }\n        }\n\n        return $user;\n    }\n\n    /**\n     * validateUserModel perform additional checks on the user model.\n     * @param object $user\n     * @return bool\n     */\n    protected function validateUserModel($user)\n    {\n        return $user instanceof $this->userModel;\n    }\n}\n"
  },
  {
    "path": "src/Auth/Manager.php",
    "content": "<?php namespace October\\Rain\\Auth;\n\nuse Request;\nuse Illuminate\\Contracts\\Auth\\StatefulGuard;\n\n/**\n * Manager for authentication\n *\n * @package october\\auth\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Manager implements StatefulGuard\n{\n    use \\October\\Rain\\Support\\Traits\\Singleton;\n    use \\October\\Rain\\Auth\\Concerns\\HasUser;\n    use \\October\\Rain\\Auth\\Concerns\\HasSession;\n    use \\October\\Rain\\Auth\\Concerns\\HasThrottle;\n    use \\October\\Rain\\Auth\\Concerns\\HasImpersonation;\n    use \\October\\Rain\\Auth\\Concerns\\HasStatefulGuard;\n    use \\October\\Rain\\Auth\\Concerns\\HasProviderProxy;\n    use \\October\\Rain\\Auth\\Concerns\\HasGuard;\n\n    /**\n     * @var Models\\User user that is currently logged in\n     */\n    protected $user;\n\n    /**\n     * @var array throttle cache in memory\n     * [md5($userId.$ipAddress) => $this->throttleModel]\n     */\n    protected $throttle = [];\n\n    /**\n     * @var string userModel class\n     */\n    protected $userModel = Models\\User::class;\n\n    /**\n     * @var string roleModel class\n     */\n    protected $roleModel = Models\\Role::class;\n\n    /**\n     * @var string groupModel class\n     */\n    protected $groupModel = Models\\Group::class;\n\n    /**\n     * @var string throttleModel class\n     */\n    protected $throttleModel = Models\\Throttle::class;\n\n    /**\n     * @var bool useThrottle flag to enable login throttling.\n     */\n    protected $useThrottle = true;\n\n    /**\n     * @var bool useRehash flag to enable password rehashing.\n     */\n    protected $useRehash = true;\n\n    /**\n     * @var bool useSession internal flag to toggle using the session for\n     * the current authentication request.\n     */\n    protected $useSession = true;\n\n    /**\n     * @var bool requireActivation rlag to require users to be activated to login.\n     */\n    protected $requireActivation = true;\n\n    /**\n     * @var string sessionKey to store the auth session data in.\n     */\n    protected $sessionKey = 'october_auth';\n\n    /**\n     * @var bool viaRemember indicates if the user was authenticated via a recaller cookie.\n     */\n    protected $viaRemember = false;\n\n    /**\n     * @var string ipAddress of this request.\n     */\n    public $ipAddress = '0.0.0.0';\n\n    /**\n     * @var bool|null checkCache adds a specific cache to the check() method to reduce\n     * the number of database calls.\n     */\n    protected $checkCache = null;\n\n    /**\n     * init the singleton\n     */\n    protected function init()\n    {\n        $this->ipAddress = Request::ip();\n    }\n\n    /**\n     * register a user with the provided credentials with optional flags for\n     * activating the newly created user and automatically logging them in.\n     *\n     * @param array $credentials\n     * @param bool $activate\n     * @param bool $autoLogin\n     * @return Models\\User\n     */\n    public function register(array $credentials, $activate = false, $autoLogin = true)\n    {\n        $user = $this->createUserModel();\n        $user->fill($credentials);\n        $user->save();\n\n        if ($activate) {\n            $user->attemptActivation($user->getActivationCode());\n        }\n\n        // Prevents revalidation of the password field\n        // on subsequent saves to this model object\n        $user->password = null;\n\n        if ($autoLogin) {\n            $this->user = $user;\n        }\n\n        return $user;\n    }\n\n    /**\n     * authenticate the given user according to the passed credentials\n     */\n    public function authenticate(array $credentials, $remember = true)\n    {\n        $user = $this->validateInternal($credentials);\n\n        $user->clearResetPassword();\n\n        $this->login($user, $remember);\n\n        return $this->user;\n    }\n\n    /**\n     * validateInternal a user's credentials, method used internally.\n     * @return Models\\User\n     */\n    protected function validateInternal(array $credentials = [])\n    {\n        // Default to the login name field or fallback to a hard-coded 'login' value\n        $loginName = $this->createUserModel()->getLoginName();\n        $loginCredentialKey = isset($credentials[$loginName]) ? $loginName : 'login';\n\n        if (empty($credentials[$loginCredentialKey])) {\n            throw new AuthException(\"The {$loginCredentialKey} attribute is required.\", 100);\n        }\n\n        if (empty($credentials['password'])) {\n            throw new AuthException('The password attribute is required.', 102);\n        }\n\n        // If the fallback 'login' was provided and did not match the necessary\n        // login name, swap it over\n        if ($loginCredentialKey !== $loginName) {\n            $credentials[$loginName] = $credentials[$loginCredentialKey];\n            unset($credentials[$loginCredentialKey]);\n        }\n\n        // If throttling is enabled, check they are not locked out first and foremost.\n        if ($this->useThrottle) {\n            $throttle = $this->findThrottleByLogin($credentials[$loginName], $this->ipAddress);\n            $throttle->check();\n        }\n\n        // Look up the user by authentication credentials.\n        try {\n            $user = $this->findUserByCredentials($credentials);\n        }\n        catch (AuthException $ex) {\n            if ($this->useThrottle) {\n                $throttle->addLoginAttempt();\n            }\n\n            throw $ex;\n        }\n\n        if ($this->useThrottle) {\n            $throttle->clearLoginAttempts();\n        }\n\n        // Rehash password if needed\n        if ($this->useRehash) {\n            $user->attemptRehashPassword($credentials['password']);\n        }\n\n        return $user;\n    }\n}\n"
  },
  {
    "path": "src/Auth/Migrations/2013_10_01_000001_Db_Users.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nreturn new class extends Migration\n{\n    public function up()\n    {\n        Schema::create('users', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('first_name')->nullable();\n            $table->string('last_name')->nullable();\n            $table->string('login')->unique()->index();\n            $table->string('email')->unique();\n            $table->string('password');\n            $table->string('activation_code')->nullable()->index();\n            $table->string('persist_code')->nullable();\n            $table->string('reset_password_code')->nullable()->index();\n            $table->text('permissions')->nullable();\n            $table->boolean('is_activated')->default(0);\n            $table->boolean('is_superuser')->default(false);\n            $table->timestamp('activated_at')->nullable();\n            $table->timestamp('last_login')->nullable();\n            $table->integer('role_id')->unsigned()->nullable()->index();\n            $table->timestamps();\n        });\n    }\n\n    public function down()\n    {\n        Schema::drop('users');\n    }\n};\n"
  },
  {
    "path": "src/Auth/Migrations/2013_10_01_000002_Db_Groups.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nreturn new class extends Migration\n{\n    public function up()\n    {\n        Schema::create('groups', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('name')->unique();\n            $table->string('code')->nullable()->index();\n            $table->text('description')->nullable();\n            $table->timestamps();\n        });\n    }\n\n    public function down()\n    {\n        Schema::drop('groups');\n    }\n};\n"
  },
  {
    "path": "src/Auth/Migrations/2013_10_01_000003_Db_Users_Groups.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nreturn new class extends Migration\n{\n    public function up()\n    {\n        Schema::create('users_groups', function (Blueprint $table) {\n            $table->integer('user_id')->unsigned();\n            $table->integer('group_id')->unsigned();\n            $table->primary(['user_id', 'group_id']);\n        });\n    }\n\n    public function down()\n    {\n        Schema::drop('users_groups');\n    }\n};\n"
  },
  {
    "path": "src/Auth/Migrations/2013_10_01_000004_Db_Preferences.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nreturn new class extends Migration\n{\n    public function up()\n    {\n        Schema::create('preferences', function (Blueprint $table) {\n            $table->increments('id');\n            $table->integer('user_id')->unsigned();\n            $table->string('namespace', 100);\n            $table->string('group', 50);\n            $table->string('item', 150);\n            $table->text('value')->nullable();\n            $table->index(['user_id', 'namespace', 'group', 'item'], 'user_item_index');\n        });\n    }\n\n    public function down()\n    {\n        Schema::drop('preferences');\n    }\n};\n"
  },
  {
    "path": "src/Auth/Migrations/2013_10_01_000005_Db_Throttle.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nreturn new class extends Migration\n{\n    public function up()\n    {\n        Schema::create('throttle', function (Blueprint $table) {\n            $table->increments('id');\n            $table->integer('user_id')->unsigned()->nullable()->index();\n            $table->string('ip_address')->nullable()->index();\n            $table->integer('attempts')->default(0);\n            $table->timestamp('last_attempt_at')->nullable();\n            $table->boolean('is_suspended')->default(0);\n            $table->timestamp('suspended_at')->nullable();\n            $table->boolean('is_banned')->default(0);\n            $table->timestamp('banned_at')->nullable();\n        });\n    }\n\n    public function down()\n    {\n        Schema::drop('throttle');\n    }\n};\n"
  },
  {
    "path": "src/Auth/Migrations/2017_10_01_000006_Db_Roles.php",
    "content": "<?php\n\nuse Illuminate\\Database\\Schema\\Blueprint;\nuse Illuminate\\Database\\Migrations\\Migration;\n\nreturn new class extends Migration\n{\n    public function up()\n    {\n        Schema::create('roles', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('name')->unique();\n            $table->text('permissions')->nullable();\n            $table->timestamps();\n        });\n    }\n\n    public function down()\n    {\n        Schema::drop('roles');\n    }\n};\n"
  },
  {
    "path": "src/Auth/Models/Group.php",
    "content": "<?php namespace October\\Rain\\Auth\\Models;\n\nuse October\\Rain\\Database\\Model;\n\n/**\n * Group model\n */\nclass Group extends Model\n{\n    use \\October\\Rain\\Database\\Traits\\Validation;\n\n    /**\n     * @var string table associated with the model\n     */\n    protected $table = 'groups';\n\n    /**\n     * @var array rules for validation\n     */\n    public $rules = [\n        'name' => 'required|between:4,16|unique:groups',\n    ];\n\n    /**\n     * @var array belongsToMany relationship\n     */\n    public $belongsToMany = [\n        'users' => [User::class, 'table' => 'users_groups']\n    ];\n\n    /**\n     * @var array fillable fields\n     */\n    protected $fillable = [\n        'name',\n        'code',\n        'description',\n    ];\n\n    /**\n     * delete the group\n     * @return bool\n     */\n    public function delete()\n    {\n        $this->users()->detach();\n        return parent::delete();\n    }\n}\n"
  },
  {
    "path": "src/Auth/Models/Preferences.php",
    "content": "<?php namespace October\\Rain\\Auth\\Models;\n\nuse October\\Rain\\Database\\Model;\nuse October\\Rain\\Auth\\AuthException;\nuse October\\Rain\\Auth\\Manager;\n\n/**\n * Preferences model for a user\n */\nclass Preferences extends Model\n{\n    use \\October\\Rain\\Support\\Traits\\KeyParser;\n\n    /**\n     * @var string table used by the model\n     */\n    protected $table = 'preferences';\n\n    /**\n     * @var bool timestamps enabled\n     */\n    public $timestamps = false;\n\n    /**\n     * @var array cache\n     */\n    protected static $cache = [];\n\n    /**\n     * @var array jsonable attribute names that are json encoded and decoded from the database\n     */\n    protected $jsonable = ['value'];\n\n    /**\n     * @var \\October\\Rain\\Auth\\Models\\User userContext is the user that owns the preferences\n     */\n    public $userContext;\n\n    /**\n     * resolveUser checks for a supplied user or uses the default logged in. You should\n     * override this method\n     *\n     * @param mixed $user An optional backend user object.\n     * @return User object\n     */\n    public function resolveUser($user)\n    {\n        $user = Manager::instance()->getUser();\n        if (!$user) {\n            throw new AuthException('User is not logged in', 400);\n        }\n\n        return $user;\n    }\n\n    /**\n     * forUser creates this object and sets the user context\n     */\n    public static function forUser($user = null)\n    {\n        $self = new static;\n        $self->userContext = $user ?: $self->resolveUser($user);\n        return $self;\n    }\n\n    /**\n     * get returns a setting value by the module (or plugin) name and setting name\n     * @param string $key Specifies the setting key value, for example 'backend:items.perpage'\n     * @param mixed $default The default value to return if the setting doesn't exist in the DB.\n     * @return mixed Returns the setting value loaded from the database or the default value.\n     */\n    public function get($key, $default = null)\n    {\n        if (!($user = $this->userContext)) {\n            return $default;\n        }\n\n        $cacheKey = $this->getCacheKey($key, $user);\n\n        if (array_key_exists($cacheKey, static::$cache)) {\n            return static::$cache[$cacheKey];\n        }\n\n        $record = static::findRecord($key, $user);\n        if (!$record) {\n            return static::$cache[$cacheKey] = $default;\n        }\n\n        return static::$cache[$cacheKey] = $record->value;\n    }\n\n    /**\n     * set stores a setting value to the database\n     * @param string $key Specifies the setting key value, for example 'backend:items.perpage'\n     * @param mixed $value The setting value to store, serializable.\n     * If the user is not provided the currently authenticated user will be used. If there is no\n     * an authenticated user, the exception will be thrown.\n     * @return bool\n     */\n    public function set($key, $value)\n    {\n        if (!$user = $this->userContext) {\n            return false;\n        }\n\n        $record = static::findRecord($key, $user);\n        if (!$record) {\n            list($namespace, $group, $item) = $this->parseKey($key);\n            $record = new static;\n            $record->namespace = $namespace;\n            $record->group = $group;\n            $record->item = $item;\n            $record->user_id = $user->id;\n        }\n\n        $record->value = $value;\n        $record->save();\n\n        $cacheKey = $this->getCacheKey($key, $user);\n        static::$cache[$cacheKey] = $value;\n        return true;\n    }\n\n    /**\n     * reset a setting value by deleting the record\n     * @param string $key Specifies the setting key value.\n     * @return bool\n     */\n    public function reset($key)\n    {\n        if (!$user = $this->userContext) {\n            return false;\n        }\n\n        $record = static::findRecord($key, $user);\n        if (!$record) {\n            return false;\n        }\n\n        $record->delete();\n\n        $cacheKey = $this->getCacheKey($key, $user);\n        unset(static::$cache[$cacheKey]);\n\n        return true;\n    }\n\n    /**\n     * findRecord returns a record for a user\n     * @return self\n     */\n    public static function findRecord($key, $user = null)\n    {\n        return static::applyKeyAndUser($key, $user)->first();\n    }\n\n    /**\n     * scopeApplyKeyAndUser to find a setting record for the specified module (or plugin) name,\n     * setting name and user.\n     *\n     * @param string $key Specifies the setting key value, for example 'backend:items.perpage'\n     * @param mixed $default The default value to return if the setting doesn't exist in the DB.\n     * @param mixed $user An optional user object.\n     * @return mixed Returns the found record or null.\n     */\n    public function scopeApplyKeyAndUser($query, $key, $user = null)\n    {\n        list($namespace, $group, $item) = $this->parseKey($key);\n\n        $query = $query\n            ->where('namespace', $namespace)\n            ->where('group', $group)\n            ->where('item', $item);\n\n        if ($user) {\n            $query = $query->where('user_id', $user->id);\n        }\n\n        return $query;\n    }\n\n    /**\n     * getCacheKey builds a cache key for the preferences record\n     * @return string\n     */\n    protected function getCacheKey($item, $user)\n    {\n        return $user->id . '-' . $item;\n    }\n}\n"
  },
  {
    "path": "src/Auth/Models/Role.php",
    "content": "<?php namespace October\\Rain\\Auth\\Models;\n\nuse InvalidArgumentException;\nuse October\\Rain\\Database\\Model;\n\n/**\n * Role model\n */\nclass Role extends Model\n{\n    use \\October\\Rain\\Database\\Traits\\Validation;\n\n    /**\n     * @var string table associated with the model\n     */\n    protected $table = 'roles';\n\n    /**\n     * @var array rules for validation\n     */\n    public $rules = [\n        'name' => 'required|between:4,16|unique:role',\n    ];\n\n    /**\n     * @var array hasMany relationship\n     */\n    public $hasMany = [\n        'users' => User::class\n    ];\n\n    /**\n     * @var array jsonable attribute names that are json encoded and decoded from the database\n     */\n    protected $jsonable = ['permissions'];\n\n    /**\n     * @var array allowedPermissionsValues\n     *\n     * Possible options:\n     *    0 => Remove.\n     *    1 => Add.\n     */\n    protected $allowedPermissionsValues = [0, 1];\n\n    /**\n     * @var array fillable fields\n     */\n    protected $fillable = [\n        'name',\n    ];\n\n    /**\n     * hasAccess will see if a role has access to the passed permission(s)\n     *\n     * If multiple permissions are passed, the role must\n     * have access to all permissions passed through, unless the\n     * \"all\" flag is set to false.\n     *\n     * @param  string|array  $permissions\n     * @param  bool  $all\n     * @return bool\n     */\n    public function hasAccess($permissions, $all = true)\n    {\n        $rolePermissions = $this->permissions;\n\n        if (!is_array($permissions)) {\n            $permissions = (array) $permissions;\n        }\n\n        foreach ($permissions as $permission) {\n            // We will set a flag now for whether this permission was\n            // matched at all.\n            $matched = true;\n\n            // Now, let's check if the permission ends in a wildcard \"*\" symbol.\n            // If it does, we'll check through all the merged permissions to see\n            // if a permission exists which matches the wildcard.\n            if ((strlen($permission) > 1) && str_ends_with($permission, '*')) {\n                $matched = false;\n\n                foreach ($rolePermissions as $rolePermission => $value) {\n                    // Strip the '*' off the end of the permission.\n                    $checkPermission = substr($permission, 0, -1);\n\n                    // We will make sure that the merged permission does not\n                    // exactly match our permission, but starts with it.\n                    if (\n                        $checkPermission !== $rolePermission &&\n                        str_starts_with($rolePermission, $checkPermission) &&\n                        (int) $value === 1\n                    ) {\n                        $matched = true;\n                        break;\n                    }\n                }\n            }\n            // Now, let's check if the permission starts in a wildcard \"*\" symbol.\n            // If it does, we'll check through all the merged permissions to see\n            // if a permission exists which matches the wildcard.\n            elseif ((strlen($permission) > 1) && str_starts_with($permission, '*')) {\n                $matched = false;\n\n                foreach ($rolePermissions as $rolePermission => $value) {\n                    // Strip the '*' off the start of the permission.\n                    $checkPermission = substr($permission, 1);\n\n                    // We will make sure that the merged permission does not\n                    // exactly match our permission, but ends with it.\n                    if (\n                        $checkPermission !== $rolePermission &&\n                        str_ends_with($rolePermission, $checkPermission) &&\n                        (int) $value === 1\n                    ) {\n                        $matched = true;\n                        break;\n                    }\n                }\n            }\n            else {\n                $matched = false;\n\n                foreach ($rolePermissions as $rolePermission => $value) {\n                    // This time check if the rolePermission ends in wildcard \"*\" symbol.\n                    if ((strlen($rolePermission) > 1) && str_ends_with($rolePermission, '*')) {\n                        $matched = false;\n\n                        // Strip the '*' off the end of the permission.\n                        $checkGroupPermission = substr($rolePermission, 0, -1);\n\n                        // We will make sure that the merged permission does not\n                        // exactly match our permission, but starts with it.\n                        if (\n                            $checkGroupPermission !== $permission &&\n                            str_starts_with($permission, $checkGroupPermission) &&\n                            (int) $value === 1\n                        ) {\n                            $matched = true;\n                            break;\n                        }\n                    }\n                    // Otherwise, we'll fallback to standard permissions checking where\n                    // we match that permissions explicitly exist.\n                    elseif (\n                        $permission === $rolePermission &&\n                        (int) $rolePermissions[$permission] === 1\n                    ) {\n                        $matched = true;\n                        break;\n                    }\n                }\n            }\n\n            // Now, we will check if we have to match all\n            // permissions or any permission and return\n            // accordingly.\n            if ($all === true && $matched === false) {\n                return false;\n            }\n            elseif ($all === false && $matched === true) {\n                return true;\n            }\n        }\n\n        return !($all === false);\n    }\n\n    /**\n     * hasAnyAccess returns if the user has access to any of the given permissions\n     * @param array $permissions\n     * @return bool\n     */\n    public function hasAnyAccess(array $permissions)\n    {\n        return $this->hasAccess($permissions, false);\n    }\n\n    /**\n     * setPermissionsAttribute validates the permissions when set\n     * @param  string  $permissions\n     * @return void\n     */\n    public function setPermissionsAttribute($permissions)\n    {\n        $permissions = json_decode($permissions, true);\n        foreach ($permissions as $permission => $value) {\n            if (!in_array($value = (int) $value, $this->allowedPermissionsValues)) {\n                throw new InvalidArgumentException(sprintf(\n                    'Invalid value \"%s\" for permission \"%s\" given.',\n                    $value,\n                    $permission\n                ));\n            }\n\n            if ($value === 0) {\n                unset($permissions[$permission]);\n            }\n        }\n\n        $this->attributes['permissions'] = !empty($permissions) ? json_encode($permissions) : '';\n    }\n}\n"
  },
  {
    "path": "src/Auth/Models/Throttle.php",
    "content": "<?php namespace October\\Rain\\Auth\\Models;\n\nuse Carbon\\Carbon;\nuse October\\Rain\\Auth\\AuthException;\nuse October\\Rain\\Database\\Model;\n\n/**\n * Throttle model\n */\nclass Throttle extends Model\n{\n    /**\n     * @var bool enabled throttling status\n     */\n    protected $enabled = true;\n\n    /**\n     * @var string table associated with the model\n     */\n    protected $table = 'throttle';\n\n    /**\n     * @var array belongsTo relation\n     */\n    public $belongsTo = [\n        'user' => [User::class, 'key' => 'user_id']\n    ];\n\n    /**\n     * @var bool timestamps indicates if the model should be timestamped\n     */\n    public $timestamps = false;\n\n    /**\n     * @var array dates attributes that should be mutated to dates\n     */\n    protected $dates = ['last_attempt_at', 'suspended_at', 'banned_at'];\n\n    /**\n     * @var int attemptLimit\n     */\n    protected static $attemptLimit = 5;\n\n    /**\n     * @var int suspensionTime in minutes\n     */\n    protected static $suspensionTime = 15;\n\n    /**\n     * getUser returns the associated user with the throttler\n     * @return User\n     */\n    public function getUser()\n    {\n        return $this->user()->getResults();\n    }\n\n    /**\n     * getLoginAttempts\n     * @return int\n     */\n    public function getLoginAttempts()\n    {\n        if ($this->attempts > 0 && $this->last_attempt_at) {\n            $this->clearLoginAttemptsIfAllowed();\n        }\n\n        return (int) $this->attempts;\n    }\n\n    /**\n     * addLoginAttempt\n     */\n    public function addLoginAttempt()\n    {\n        $this->attempts++;\n        $this->last_attempt_at = $this->freshTimestamp();\n\n        if ($this->getLoginAttempts() >= static::$attemptLimit) {\n            $this->suspend();\n        }\n        else {\n            $this->save();\n        }\n    }\n\n    /**\n     * clearLoginAttempts\n     */\n    public function clearLoginAttempts()\n    {\n        // If our login attempts is already at zero we do not need to do anything. Additionally,\n        // if we are suspended, we are not going to do anything either as clearing login attempts\n        // makes us unsuspended. We need to manually call unsuspend() in order to unsuspend.\n        if ($this->getLoginAttempts() === 0 || $this->is_suspended) {\n            return;\n        }\n\n        $this->attempts = 0;\n        $this->last_attempt_at = null;\n        $this->is_suspended = false;\n        $this->suspended_at = null;\n        $this->save();\n    }\n\n    /**\n     * suspend the user associated with the throttle\n     */\n    public function suspend()\n    {\n        if (!$this->is_suspended) {\n            $this->is_suspended = true;\n            $this->suspended_at = $this->freshTimestamp();\n            $this->save();\n        }\n    }\n\n    /**\n     * unsuspend the user\n     */\n    public function unsuspend()\n    {\n        if ($this->is_suspended) {\n            $this->attempts = 0;\n            $this->last_attempt_at = null;\n            $this->is_suspended = false;\n            $this->suspended_at = null;\n            $this->save();\n        }\n    }\n\n    /**\n     * checkSuspended checks if the user is suspended\n     * @return bool\n     */\n    public function checkSuspended()\n    {\n        if ($this->is_suspended && $this->suspended_at) {\n            $this->removeSuspensionIfAllowed();\n            return (bool) $this->is_suspended;\n        }\n\n        return false;\n    }\n\n    /**\n     * ban the user\n     * @return void\n     */\n    public function ban()\n    {\n        if (!$this->is_banned) {\n            $this->is_banned = true;\n            $this->banned_at = $this->freshTimestamp();\n            $this->save();\n        }\n    }\n\n    /**\n     * unban the user\n     * @return void\n     */\n    public function unban()\n    {\n        if ($this->is_banned) {\n            $this->is_banned = false;\n            $this->banned_at = null;\n            $this->save();\n        }\n    }\n\n    /**\n     * check user throttle status\n     * @return bool\n     * @throws AuthException\n     */\n    public function check()\n    {\n        if ($this->is_banned) {\n            throw new AuthException('Cannot login user since they are banned.', 302);\n        }\n\n        if ($this->checkSuspended()) {\n            throw new AuthException('Cannot login user since they are suspended.', 301);\n        }\n\n        return true;\n    }\n\n    /**\n     * clearLoginAttemptsIfAllowed inspects the last attempt vs the suspension time\n     * (the time in which attempts must space before the account is suspended).\n     * If we can clear our attempts now, we'll do so and save.\n     *\n     * @return void\n     */\n    public function clearLoginAttemptsIfAllowed()\n    {\n        $lastAttempt = clone $this->last_attempt_at;\n\n        $suspensionTime = static::$suspensionTime;\n        $clearAttemptsAt = $lastAttempt->modify(\"+{$suspensionTime} minutes\");\n        $now = new Carbon;\n\n        if ($clearAttemptsAt <= $now) {\n            $this->attempts = 0;\n            $this->save();\n        }\n\n        unset($lastAttempt, $clearAttemptsAt, $now);\n    }\n\n    /**\n     * removeSuspensionIfAllowed inspects to see if the user can become unsuspended\n     * or not, based on the suspension time provided. If so, unsuspends.\n     */\n    public function removeSuspensionIfAllowed()\n    {\n        $suspended = clone $this->suspended_at;\n\n        $suspensionTime = static::$suspensionTime;\n        $unsuspendAt = $suspended->modify(\"+{$suspensionTime} minutes\");\n        $now = new Carbon;\n\n        if ($unsuspendAt <= $now) {\n            $this->unsuspend();\n        }\n\n        unset($suspended, $unsuspendAt, $now);\n    }\n\n    /**\n     * getIsSuspendedAttribute is a get mutator for the suspended property\n     * @param  mixed  $suspended\n     * @return bool\n     */\n    public function getIsSuspendedAttribute($suspended)\n    {\n        return (bool) $suspended;\n    }\n\n    /**\n     * getIsBannedAttribute is a get mutator for the banned property\n     * @param  mixed  $banned\n     * @return bool\n     */\n    public function getIsBannedAttribute($banned)\n    {\n        return (bool) $banned;\n    }\n}\n"
  },
  {
    "path": "src/Auth/Models/User.php",
    "content": "<?php namespace October\\Rain\\Auth\\Models;\n\nuse Str;\nuse Hash;\nuse October\\Rain\\Database\\Model;\nuse Illuminate\\Contracts\\Auth\\Authenticatable;\nuse InvalidArgumentException;\nuse Exception;\n\n/**\n * User model\n */\nclass User extends Model implements Authenticatable\n{\n    use \\October\\Rain\\Database\\Traits\\Hashable;\n    use \\October\\Rain\\Database\\Traits\\Purgeable;\n    use \\October\\Rain\\Database\\Traits\\Validation;\n\n    /**\n     * @var string table associated with the model\n     */\n    protected $table = 'users';\n\n    /**\n     * @var array rules for validation\n     */\n    public $rules = [\n        'email' => 'required|between:3,255|email|unique:users',\n        'password' => 'required:create|min:2|confirmed',\n        'password_confirmation' => 'required_with:password|min:2'\n    ];\n\n    /**\n     * @var array belongsToMany relation\n     */\n    public $belongsToMany = [\n        'groups' => [Group::class, 'table' => 'users_groups']\n    ];\n\n    /**\n     * @var array belongsTo relation\n     */\n    public $belongsTo = [\n        'role' => Role::class\n    ];\n\n    /**\n     * @var array dates attributes that should be mutated to dates\n     */\n    protected $dates = ['activated_at', 'last_login'];\n\n    /**\n     * @var array hidden attributes removed from the API representation of the model (ex. toArray())\n     */\n    protected $hidden = ['password', 'reset_password_code', 'activation_code', 'persist_code'];\n\n    /**\n     * @var array fillable fields\n     */\n    protected $fillable = [\n        'first_name',\n        'last_name',\n        'login',\n        'email',\n        'password',\n        'password_confirmation',\n    ];\n\n    /**\n     * @var array hashable list of attribute names which should be hashed using the Bcrypt hashing algorithm\n     */\n    protected $hashable = ['password', 'persist_code'];\n\n    /**\n     * @var array purgeable list of attribute names which should not be saved to the database\n     */\n    protected $purgeable = ['password_confirmation'];\n\n    /**\n     * @var array attributeNames array of custom attribute names\n     */\n    public $attributeNames = [];\n\n    /**\n     * @var array customMessages array of custom error messages\n     */\n    public $customMessages = [];\n\n    /**\n     * @var array jsonable attribute names that are json encoded and decoded from the database\n     */\n    protected $jsonable = ['permissions'];\n\n    /**\n     * allowedPermissionsValues\n     *\n     * Possible options:\n     *   -1 => Deny (adds to array, but denies regardless of user's group).\n     *    0 => Remove.\n     *    1 => Add.\n     *\n     * @var array\n     */\n    protected $allowedPermissionsValues = [-1, 0, 1];\n\n    /**\n     * @var string loginAttribute\n     */\n    public static $loginAttribute = 'email';\n\n    /**\n     * @var string rememberTokenName is the column name of the \"remember me\" token\n     */\n    protected $rememberTokenName = 'persist_code';\n\n    /**\n     * @var array mergedPermissions for the user\n     */\n    protected $mergedPermissions;\n\n    /**\n     * @var Role impersonatingRole set if the user is impersonating a role\n     */\n    protected $impersonatingRole;\n\n    /**\n     * @return string getLoginName returns the name for the user's login\n     */\n    public function getLoginName()\n    {\n        return static::$loginAttribute;\n    }\n\n    /**\n     * @return mixed getLogin returns the user's login\n     */\n    public function getLogin()\n    {\n        return $this->{$this->getLoginName()};\n    }\n\n    /**\n     * isSuperUser checks if the user is a super user - has access to everything\n     * regardless of permissions\n     * @return bool\n     */\n    public function isSuperUser()\n    {\n        if ($this->impersonatingRole) {\n            return false;\n        }\n\n        return (bool) $this->is_superuser;\n    }\n\n    //\n    // Events\n    //\n\n    /**\n     * beforeLogin event\n     */\n    public function beforeLogin()\n    {\n    }\n\n    /**\n     * afterLogin event\n     */\n    public function afterLogin()\n    {\n        $this->last_login = $this->freshTimestamp();\n        $this->forceSave();\n    }\n\n    /**\n     * afterDelete deletes the user groups\n     * @return bool\n     */\n    public function afterDelete()\n    {\n        if ($this->hasRelation('groups')) {\n            $this->groups()->detach();\n        }\n    }\n\n    //\n    // Persistence (used by Cookies and Sessions)\n    //\n\n    /**\n     * getPersistCode gets a code for when the user is persisted to a cookie or session\n     * which identifies the user\n     * @return string\n     */\n    public function getPersistCode()\n    {\n        $this->persist_code = $this->getRandomString();\n\n        // Our code got hashed\n        $persistCode = $this->persist_code;\n\n        $this->forceSave();\n\n        return $persistCode;\n    }\n\n    /**\n     * checkPersistCode checks the given persist code\n     * @param string $persistCode\n     * @return bool\n     */\n    public function checkPersistCode($persistCode)\n    {\n        if (!$persistCode || !$this->persist_code) {\n            return false;\n        }\n\n        return $persistCode === $this->persist_code;\n    }\n\n    //\n    // Activation\n    //\n\n    /**\n     * getIsActivatedAttribute is a get mutator for giving the activated property\n     * @param mixed $activated\n     * @return bool\n     */\n    public function getIsActivatedAttribute($activated)\n    {\n        return (bool) $activated;\n    }\n\n    /**\n     * getActivationCode for the given user\n     * @return string\n     */\n    public function getActivationCode()\n    {\n        $this->activation_code = $activationCode = $this->getRandomString();\n\n        $this->forceSave();\n\n        return $activationCode;\n    }\n\n    /**\n     * attemptActivation the given user by checking the activate code. If the user\n     * is activated already, an Exception is thrown\n     * @param string $activationCode\n     * @return bool\n     */\n    public function attemptActivation($activationCode)\n    {\n        if ($this->is_activated) {\n            throw new Exception('User is already active!');\n        }\n\n        if ($activationCode === $this->activation_code) {\n            $this->activation_code = null;\n            $this->is_activated = true;\n            $this->activated_at = $this->freshTimestamp();\n            $this->forceSave();\n            return true;\n        }\n\n        return false;\n    }\n\n    //\n    // Password\n    //\n\n    /**\n     * checkPassword checks the password passed matches the user's password\n     * @param string $password\n     * @return bool\n     */\n    public function checkPassword($password)\n    {\n        return Hash::check($password, $this->password);\n    }\n\n    /**\n     * getResetPasswordCode gets a reset password code for the given user\n     * @return string\n     */\n    public function getResetPasswordCode()\n    {\n        $this->reset_password_code = $resetCode = $this->getRandomString();\n        $this->forceSave();\n        return $resetCode;\n    }\n\n    /**\n     * checkResetPasswordCode checks if the provided user reset password code is\n     * valid without actually resetting the password\n     * @param string $resetCode\n     * @return bool\n     */\n    public function checkResetPasswordCode($resetCode)\n    {\n        if (!$resetCode || !$this->reset_password_code) {\n            return false;\n        }\n\n        return $this->reset_password_code === $resetCode;\n    }\n\n    /**\n     * attemptResetPassword a user's password by matching the reset code generated with the users\n     * @param string $resetCode\n     * @param string $newPassword\n     * @return bool\n     */\n    public function attemptResetPassword($resetCode, $newPassword)\n    {\n        if ($this->checkResetPasswordCode($resetCode)) {\n            $this->password = $newPassword;\n            $this->reset_password_code = null;\n\n            if ($this->is_password_expired) {\n                $this->is_password_expired = false;\n            }\n\n            return $this->forceSave();\n        }\n\n        return false;\n    }\n\n    /**\n     * attemptRehashPassword will check if a password needs to be rehashed and apply the\n     * new hashing algorithm to the current password supplied as plaintext.\n     */\n    public function attemptRehashPassword(string $currentPassword): bool\n    {\n        if (!Hash::needsRehash($this->password)) {\n            return false;\n        }\n\n        if (!$this->checkPassword($currentPassword)) {\n            throw new Exception('Cannot rehash using a new password!');\n        }\n\n        // Rehash via the Hashable trait\n        $this->password = $currentPassword;\n\n        return $this->forceSave();\n    }\n\n    /**\n     * clearResetPassword wipes out the data associated with resetting a password\n     * @return void\n     */\n    public function clearResetPassword()\n    {\n        if ($this->reset_password_code) {\n            $this->reset_password_code = null;\n            $this->forceSave();\n        }\n    }\n\n    /**\n     * setPasswordAttribute protects the password from being reset to null\n     */\n    public function setPasswordAttribute($value)\n    {\n        if ($this->exists && empty($value)) {\n            unset($this->attributes['password']);\n        }\n        else {\n            $this->attributes['password'] = $value;\n\n            // Password has changed, log out all users\n            $this->attributes['persist_code'] = null;\n        }\n    }\n\n    //\n    // Permissions, Groups & Role\n    //\n\n    /**\n     * getGroups returns an array of groups which the given user belongs to\n     * @return array\n     */\n    public function getGroups()\n    {\n        return $this->groups;\n    }\n\n    /**\n     * getRole returns the role assigned to this user\n     * @return October\\Rain\\Auth\\Models\\Role\n     */\n    public function getRole()\n    {\n        return $this->role;\n    }\n\n    /**\n     * setRoleImpersonation set to the role to impersonate or null to disable.\n     */\n    public function setRoleImpersonation(?Role $role): void\n    {\n        $this->impersonatingRole = $role;\n    }\n\n    /**\n     * getRoleImpersonation\n     */\n    public function getRoleImpersonation(): ?Role\n    {\n        return $this->impersonatingRole;\n    }\n\n    /**\n     * addGroup adds the user to the given group\n     * @param Group $group\n     * @return bool\n     */\n    public function addGroup($group)\n    {\n        if (!$this->inGroup($group)) {\n            $this->groups()->attach($group);\n            $this->unsetRelation('groups');\n        }\n\n        return true;\n    }\n\n    /**\n     * removeGroup removes the user from the given group\n     * @param Group $group\n     * @return bool\n     */\n    public function removeGroup($group)\n    {\n        if ($this->inGroup($group)) {\n            $this->groups()->detach($group);\n            $this->unsetRelation('groups');\n        }\n\n        return true;\n    }\n\n    /**\n     * inGroup see if the user is in the given group\n     * @param Group $group\n     * @return bool\n     */\n    public function inGroup($group)\n    {\n        foreach ($this->getGroups() as $_group) {\n            if ($_group->getKey() === $group->getKey()) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * getMergedPermissions returns an array of merged permissions for each group the user is in\n     * @return array\n     */\n    public function getMergedPermissions()\n    {\n        if (!$this->mergedPermissions) {\n            $permissions = [];\n\n            if (($role = $this->getRole()) && is_array($role->permissions)) {\n                $permissions = array_merge($permissions, $role->permissions);\n            }\n\n            if (is_array($this->permissions)) {\n                $permissions = array_merge($permissions, $this->permissions);\n            }\n\n            $this->mergedPermissions = $permissions;\n        }\n\n        return $this->mergedPermissions;\n    }\n\n    /**\n     * hasAccess sees if a user has access to the passed permission(s). Permissions are merged\n     * from all groups the user belongs to and then are checked against the passed permission(s).\n     *\n     * If multiple permissions are passed, the user must have access to all permissions passed\n     * through, unless the \"all\" flag is set to false.\n     *\n     * Super users have access no matter what.\n     *\n     * @param  string|array  $permissions\n     * @param  bool  $all\n     * @return bool\n     */\n    public function hasAccess($permissions, $all = true)\n    {\n        if ($this->isSuperUser()) {\n            return true;\n        }\n\n        return $this->hasPermission($permissions, $all);\n    }\n\n    /**\n     * hasPermission sees if a user has access to the passed permission(s). Permissions are merged\n     * from all groups the user belongs to and then are checked against the passed permission(s).\n     *\n     * If multiple permissions are passed, the user must have access to all permissions passed\n     * through, unless the \"all\" flag is set to false.\n     *\n     * Super users don't have access no matter what.\n     *\n     * @param  string|array  $permissions\n     * @param  bool  $all\n     * @return bool\n     */\n    public function hasPermission($permissions, $all = true)\n    {\n        if ($this->impersonatingRole) {\n            $mergedPermissions = (array) $this->impersonatingRole->permissions;\n        }\n        else {\n            $mergedPermissions = $this->getMergedPermissions();\n        }\n\n        if (!is_array($permissions)) {\n            $permissions = [$permissions];\n        }\n\n        foreach ($permissions as $permission) {\n            // We will set a flag now for whether this permission was\n            // matched at all.\n            $matched = true;\n\n            // Now, let's check if the permission ends in a wildcard \"*\" symbol.\n            // If it does, we'll check through all the merged permissions to see\n            // if a permission exists which matches the wildcard.\n            if ((strlen($permission) > 1) && str_ends_with($permission, '*')) {\n                $matched = false;\n\n                foreach ($mergedPermissions as $mergedPermission => $value) {\n                    // Strip the '*' off the end of the permission.\n                    $checkPermission = substr($permission, 0, -1);\n\n                    // We will make sure that the merged permission does not\n                    // exactly match our permission, but starts with it.\n                    if (\n                        $checkPermission !== $mergedPermission &&\n                        str_starts_with($mergedPermission, $checkPermission) &&\n                        (int) $value === 1\n                    ) {\n                        $matched = true;\n                        break;\n                    }\n                }\n            }\n            elseif ((strlen($permission) > 1) && str_starts_with($permission, '*')) {\n                $matched = false;\n\n                foreach ($mergedPermissions as $mergedPermission => $value) {\n                    // Strip the '*' off the beginning of the permission.\n                    $checkPermission = substr($permission, 1);\n\n                    // We will make sure that the merged permission does not\n                    // exactly match our permission, but ends with it.\n                    if (\n                        $checkPermission !== $mergedPermission &&\n                        str_ends_with($mergedPermission, $checkPermission) &&\n                        (int) $value === 1\n                    ) {\n                        $matched = true;\n                        break;\n                    }\n                }\n            }\n            else {\n                $matched = false;\n\n                foreach ($mergedPermissions as $mergedPermission => $value) {\n                    // This time check if the mergedPermission ends in wildcard \"*\" symbol.\n                    if ((strlen($mergedPermission) > 1) && str_ends_with($mergedPermission, '*')) {\n                        $matched = false;\n\n                        // Strip the '*' off the end of the permission.\n                        $checkMergedPermission = substr($mergedPermission, 0, -1);\n\n                        // We will make sure that the merged permission does not\n                        // exactly match our permission, but starts with it.\n                        if (\n                            $checkMergedPermission !== $permission &&\n                            str_starts_with($permission, $checkMergedPermission) &&\n                            (int) $value === 1\n                        ) {\n                            $matched = true;\n                            break;\n                        }\n                    }\n                    // Otherwise, we'll fallback to standard permissions checking where\n                    // we match that permissions explicitly exist.\n                    elseif (\n                        $permission === $mergedPermission &&\n                        (int) $mergedPermissions[$permission] === 1\n                    ) {\n                        $matched = true;\n                        break;\n                    }\n                }\n            }\n\n            // Now, we will check if we have to match all permissions or any permission and return\n            // accordingly.\n            if ($all === true && $matched === false) {\n                return false;\n            }\n            elseif ($all === false && $matched === true) {\n                return true;\n            }\n        }\n\n        return !($all === false);\n    }\n\n    /**\n     * hasAnyAccess returns if the user has access to any of the given permissions\n     * @param  array  $permissions\n     * @return bool\n     */\n    public function hasAnyAccess(array $permissions)\n    {\n        return $this->hasAccess($permissions, false);\n    }\n\n    /**\n     * setPermissionsAttribute validates any set permissions\n     * @param string $permissions\n     * @return void\n     */\n    public function setPermissionsAttribute($permissions)\n    {\n        $permissions = json_decode($permissions, true) ?: [];\n        foreach ($permissions as $permission => &$value) {\n            if (!in_array($value = (int) $value, $this->allowedPermissionsValues)) {\n                throw new InvalidArgumentException(sprintf(\n                    'Invalid value \"%s\" for permission \"%s\" given.',\n                    $value,\n                    $permission\n                ));\n            }\n\n            if ($value === 0) {\n                unset($permissions[$permission]);\n            }\n        }\n\n        $this->attributes['permissions'] = !empty($permissions) ? json_encode($permissions) : '';\n    }\n\n    //\n    // User Interface\n    //\n\n    /**\n     * getAuthIdentifierName gets the name of the unique identifier for the user\n     * @return string\n     */\n    public function getAuthIdentifierName()\n    {\n        return $this->getKeyName();\n    }\n\n    /**\n     * getAuthPasswordName of the password attribute for the user.\n     */\n    public function getAuthPasswordName()\n    {\n        return 'password';\n    }\n\n    /**\n     * getAuthIdentifier gets the unique identifier for the user\n     * @return mixed\n     */\n    public function getAuthIdentifier()\n    {\n        return $this->{$this->getAuthIdentifierName()};\n    }\n\n    /**\n     * getAuthPassword gets the password for the user\n     * @return string\n     */\n    public function getAuthPassword()\n    {\n        return $this->password;\n    }\n\n    /**\n     * getReminderEmail gets the e-mail address where password reminders are sent\n     * @return string\n     */\n    public function getReminderEmail()\n    {\n        return $this->email;\n    }\n\n    /**\n     * getRememberToken gets the token value for the \"remember me\" session\n     * @return string\n     */\n    public function getRememberToken()\n    {\n        return $this->getPersistCode();\n    }\n\n    /**\n     * setRememberToken sets the token value for the \"remember me\" session\n     * @param  string $value\n     * @return void\n     */\n    public function setRememberToken($value)\n    {\n        $this->persist_code = $value;\n    }\n\n    /**\n     * getRememberTokenName gets the column name for the \"remember me\" token\n     * @return string\n     */\n    public function getRememberTokenName()\n    {\n        return $this->rememberTokenName;\n    }\n\n    //\n    // Helpers\n    //\n\n    /**\n     * getRandomString generates a random string\n     * @return string\n     */\n    public function getRandomString($length = 42)\n    {\n        return Str::random($length);\n    }\n}\n"
  },
  {
    "path": "src/Composer/ClassLoader.php",
    "content": "<?php namespace October\\Rain\\Composer;\n\nuse Exception;\nuse Throwable;\n\n/**\n * ClassLoader is a custom autoloader used by October CMS, it uses folder names\n * to be lower case and the file name to be capitalized as per the class name.\n */\nclass ClassLoader\n{\n    /**\n     * @var static|null loader instance\n     */\n    private static $loader = null;\n\n    /**\n     * @var string basePath\n     */\n    public $basePath;\n\n    /**\n     * @var string|null manifestPath\n     */\n    public $manifestPath;\n\n    /**\n     * @var array manifest of loaded items\n     */\n    public $manifest = [];\n\n    /**\n     * @var array unknownClasses cache\n     */\n    protected $unknownClasses = [];\n\n    /**\n     * @var bool manifestDirty if manifest needs to be written\n     */\n    protected $manifestDirty = false;\n\n    /**\n     * @var array namespaces registered\n     */\n    protected $namespaces = [];\n\n    /**\n     * @var array directories registered\n     */\n    protected $directories = [];\n\n    /**\n     * @var bool registered indicates if this class is registered\n     */\n    protected $registered = false;\n\n    /**\n     * __construct creates a new package manifest instance\n     */\n    public function __construct(string $basePath)\n    {\n        $this->basePath = $basePath;\n    }\n\n    /**\n     * instance returns the class loader instance\n     */\n    public static function instance(): ?static\n    {\n        return static::$loader;\n    }\n\n    /**\n     * configure the loader\n     */\n    public static function configure($basePath)\n    {\n        return static::$loader = new static($basePath);\n    }\n\n    /**\n     * withNamespace\n     */\n    public function withNamespace($namespace, $directory): static\n    {\n        $this->namespaces[$namespace] = $directory;\n\n        return $this;\n    }\n\n    /**\n     * withDirectories to the class loader\n     * @param string|array $directories\n     */\n    public function withDirectories($directories): static\n    {\n        $this->directories = array_merge($this->directories, (array) $directories);\n\n        $this->directories = array_unique($this->directories);\n\n        return $this;\n    }\n\n    /**\n     * load the given class file\n     * @param string $class\n     */\n    public function load($class): bool\n    {\n        if (!str_contains($class, '\\\\')) {\n            return false;\n        }\n\n        if (\n            isset($this->manifest[$class]) &&\n            is_file($fullPath = $this->basePath.DIRECTORY_SEPARATOR.$this->manifest[$class])\n        ) {\n            require $fullPath;\n            return true;\n        }\n\n        if (isset($this->unknownClasses[$class])) {\n            return false;\n        }\n\n        // Load namespaces\n        foreach ($this->namespaces as $namespace => $directory) {\n            if (substr($class, 0, strlen($namespace)) === $namespace) {\n                $classWithoutNamespace = substr($class, strlen($namespace));\n                [$lowerClass, $upperClass] = $this->normalizeClass($classWithoutNamespace);\n                if ($this->loadUpperOrLower($class, $directory, $upperClass, $lowerClass) === true) {\n                    return true;\n                }\n            }\n        }\n\n        // Load directories\n        [$lowerClass, $upperClass] = $this->normalizeClass($class);\n        foreach ($this->directories as $directory) {\n            if ($this->loadUpperOrLower($class, $directory, $upperClass, $lowerClass) === true) {\n                return true;\n            }\n        }\n\n        $this->unknownClasses[$class] = true;\n\n        return false;\n    }\n\n    /**\n     * register the given class loader on the auto-loader stack\n     */\n    public function register()\n    {\n        if ($this->registered) {\n            return;\n        }\n\n        $this->registered = spl_autoload_register(function($class) {\n            $this->load($class);\n        });\n    }\n\n    /**\n     * build the manifest and write it to disk\n     */\n    public function build()\n    {\n        if (!$this->manifestDirty) {\n            return;\n        }\n\n        $this->write($this->manifest);\n    }\n\n    /**\n     * initManifest starts the manifest cache file after registration.\n     */\n    public function initManifest(string $manifestPath)\n    {\n        $this->manifestPath = $manifestPath;\n\n        $this->ensureManifestIsLoaded();\n    }\n\n    /**\n     * removeDirectories from the class loader\n     * @param string|array $directories\n     */\n    public function removeDirectories($directories = null)\n    {\n        if (is_null($directories)) {\n            $this->directories = [];\n        }\n        else {\n            $directories = (array) $directories;\n\n            $this->directories = array_filter($this->directories, function ($directory) use ($directories) {\n                return !in_array($directory, $directories);\n            });\n        }\n    }\n\n    /**\n     * getDirectories registered with the loader\n     */\n    public function getDirectories(): array\n    {\n        return $this->directories;\n    }\n\n    /**\n     * loadUpperOrLower loads a class in a directory with the supplied upper and lower class path.\n     */\n    protected function loadUpperOrLower(string $class, string $directory, string $upperClass, string $lowerClass): bool\n    {\n        if ($directory) {\n            $directory .= DIRECTORY_SEPARATOR;\n        }\n\n        if ($this->isRealFilePath($path = $directory.$lowerClass)) {\n            $this->includeClass($class, $path);\n            return true;\n        }\n\n        if ($this->isRealFilePath($path = $directory.$upperClass)) {\n            $this->includeClass($class, $path);\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * isRealFilePath determines if a relative path to a file exists and is real\n     */\n    protected function isRealFilePath(string $path): bool\n    {\n        return is_file(realpath($this->basePath.DIRECTORY_SEPARATOR.$path));\n    }\n\n    /**\n     * includeClass and add to the manifest\n     */\n    protected function includeClass(string $class, string $path)\n    {\n        require $this->basePath.DIRECTORY_SEPARATOR.$path;\n\n        // Normalize path\n        $this->manifest[$class] = str_replace('\\\\', '/', $path);\n\n        $this->manifestDirty = true;\n    }\n\n    /**\n     * normalizeClass get the normal file name for a class\n     */\n    protected function normalizeClass(string $class): array\n    {\n        // Strip first slash\n        if ($class[0] === '\\\\') {\n            $class = substr($class, 1);\n        }\n\n        // Lowercase folders\n        $parts = explode('\\\\', $class);\n        $file = array_pop($parts);\n        $namespace = implode('\\\\', $parts);\n        $directory = str_replace(['\\\\', '_'], DIRECTORY_SEPARATOR, $namespace);\n\n        // Provide both alternatives\n        $lowerClass = strtolower($directory) . DIRECTORY_SEPARATOR . $file . '.php';\n        $upperClass = $directory . DIRECTORY_SEPARATOR . $file . '.php';\n\n        return [$lowerClass, $upperClass];\n    }\n\n    /**\n     * ensureManifestIsLoaded has been loaded into memory\n     */\n    protected function ensureManifestIsLoaded()\n    {\n        $manifest = [];\n\n        if (file_exists($this->manifestPath)) {\n            try {\n                $manifest = require $this->manifestPath;\n\n                if (!is_array($manifest)) {\n                    $manifest = [];\n                }\n            }\n            catch (Throwable $ex) {}\n        }\n\n        $this->manifest += $manifest;\n    }\n\n    /**\n     * write the given manifest array to disk\n     */\n    protected function write(array $manifest)\n    {\n        if ($this->manifestPath === null) {\n            return;\n        }\n\n        if (!is_writable(dirname($this->manifestPath))) {\n            throw new Exception(\"The directory [{$this->manifestPath}] must be present and writable.\");\n        }\n\n        file_put_contents(\n            $this->manifestPath,\n            '<?php return '.var_export($manifest, true).';'\n        );\n    }\n}\n"
  },
  {
    "path": "src/Composer/ComposerManager.php",
    "content": "<?php namespace October\\Rain\\Composer;\n\nuse App;\nuse Config;\nuse Composer\\Factory;\nuse Composer\\Composer;\nuse Composer\\Installer;\nuse Composer\\Json\\JsonFile;\nuse Composer\\Semver\\VersionParser;\nuse Composer\\Config\\JsonConfigSource;\nuse Composer\\DependencyResolver\\Request;\nuse Exception;\nuse Throwable;\n\n/**\n * ComposerManager super class for working with Composer\n *\n * @method static Manager instance()\n *\n * @package october\\composer\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ComposerManager\n{\n    use Concerns\\HasOutput;\n    use Concerns\\HasAssertions;\n    use Concerns\\HasAutoloader;\n    use Concerns\\HasRequirements;\n    use Concerns\\HasOctoberCommands;\n\n    /**\n     * __construct composer manager\n     */\n    public function __construct()\n    {\n        $this->setOutput();\n    }\n\n    /**\n     * instance creates a new instance of this singleton\n     */\n    public static function instance(): static\n    {\n        try {\n            return App::make('core.composer');\n        }\n        catch (Exception $ex) {\n            return new static;\n        }\n    }\n\n    /**\n     * update runs the \"composer update\" command\n     */\n    public function update(array $packages = [])\n    {\n        $this->assertEnvironmentReady();\n        $this->assertHomeVariableSet();\n\n        try {\n            $this->assertHomeDirectory();\n            $this->assertComposerWarmedUp();\n\n            Installer::create($this->output, $this->makeComposer())\n                ->setDevMode(Config::get('app.debug', false))\n                ->setUpdateAllowList($packages)\n                ->setPreferDist()\n                ->setUpdate(true)\n                ->run();\n        }\n        finally {\n            $this->assertWorkingDirectory();\n        }\n    }\n\n    /**\n     * require runs the \"composer require\" command\n     */\n    public function require(array $requirements)\n    {\n        $this->assertEnvironmentReady();\n        $this->assertHomeVariableSet();\n        $this->backupComposerFile();\n\n        $statusCode = 1;\n        $lastException = new Exception('Failed to update composer dependencies');\n\n        try {\n            $this->assertHomeDirectory();\n            $this->assertComposerWarmedUp();\n            $this->writePackages($requirements);\n\n            $composer = $this->makeComposer();\n            $installer = Installer::create($this->output, $composer)\n                ->setDevMode(Config::get('app.debug', false))\n                ->setPreferDist()\n                ->setUpdate(true)\n                ->setUpdateAllowTransitiveDependencies(Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS);\n\n            // If no lock is present, or the file is brand new, we do not do a\n            // partial update as this is not supported by the Installer\n            if ($composer->getLocker()->isLocked()) {\n                $installer->setUpdateAllowList(array_keys($requirements));\n            }\n\n            $statusCode = $installer->run();\n        }\n        catch (Throwable $ex) {\n            $statusCode = 1;\n            $lastException = $ex;\n        }\n        finally {\n            $this->assertWorkingDirectory();\n        }\n\n        if ($statusCode !== 0) {\n            $this->restoreComposerFile();\n            throw $lastException;\n        }\n    }\n\n    /**\n     * remove runs the \"composer remove\" command\n     */\n    public function remove(array $packageNames)\n    {\n        $requirements = [];\n        foreach ($packageNames as $package) {\n            $requirements[$package] = false;\n        }\n\n        $this->require($requirements);\n    }\n\n    /**\n     * addPackages without update\n     */\n    public function addPackages(array $requirements)\n    {\n        $this->writePackages($requirements);\n    }\n\n    /**\n     * removePackages without update\n     */\n    public function removePackages(array $packageNames)\n    {\n        $requirements = [];\n        foreach ($packageNames as $package) {\n            $requirements[$package] = false;\n        }\n\n        $this->writePackages($requirements);\n    }\n\n    /**\n     * getPackageVersions returns version numbers for the specified packages\n     */\n    public function getPackageVersions(array $packageNames): array\n    {\n        $result = [];\n        $packages = $this->listAllPackages();\n\n        foreach ($packageNames as $wantPackage) {\n            $wantPackageLower = mb_strtolower($wantPackage);\n\n            foreach ($packages as $package) {\n                if (!isset($package['name'])) {\n                    continue;\n                }\n                if (mb_strtolower($package['name']) === $wantPackageLower) {\n                    $result[$wantPackage] = $package['version'] ?? null;\n                }\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * hasPackage returns true if the specified package is installed\n     */\n    public function hasPackage($name): bool\n    {\n        $name = mb_strtolower($name);\n\n        return array_key_exists($name, $this->getPackageVersions([$name]));\n    }\n\n    /**\n     * listPackages returns a list of directly installed packages\n     */\n    public function listPackages()\n    {\n        return $this->listPackagesInternal();\n    }\n\n    /**\n     * listAllPackages returns a list of installed packages, including dependencies\n     */\n    public function listAllPackages()\n    {\n        return $this->listPackagesInternal(false);\n    }\n\n    /**\n     * addRepository will add a repository to the composer config\n     */\n    public function addRepository($name, $type, $address, $options = [])\n    {\n        $file = new JsonFile($this->getJsonPath());\n\n        $config = new JsonConfigSource($file);\n\n        $config->addRepository($name, array_merge([\n            'type' => $type,\n            'url' => $address\n        ], $options));\n    }\n\n    /**\n     * removeRepository will remove a repository from the composer config\n     */\n    public function removeRepository($name)\n    {\n        $file = new JsonFile($this->getJsonPath());\n\n        $config = new JsonConfigSource($file);\n\n        $config->removeConfigSetting($name);\n    }\n\n    /**\n     * hasRepository return true if the composer config contains the repo address\n     */\n    public function hasRepository($address): bool\n    {\n        $file = new JsonFile($this->getJsonPath());\n\n        $config = $file->read();\n\n        $repos = $config['repositories'] ?? [];\n\n        foreach ($repos as $repo) {\n            if (!isset($repo['url'])) {\n                continue;\n            }\n\n            if (rtrim($repo['url'], '/') === $address) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * addAuthCredentials will add credentials to an auth config file\n     */\n    public function addAuthCredentials($hostname, $username, $password, $type = null)\n    {\n        if ($type === null) {\n            $type = 'http-basic';\n        }\n\n        $file = new JsonFile($this->getAuthPath());\n\n        $config = new JsonConfigSource($file, true);\n\n        $config->addConfigSetting($type.'.'.$hostname, [\n            'username' => $username,\n            'password' => $password\n        ]);\n    }\n\n    /**\n     * getAuthCredentials returns auth credentials added to the config file\n     */\n    public function getAuthCredentials($hostname, $type = null): ?array\n    {\n        if ($type === null) {\n            $type = 'http-basic';\n        }\n\n        $authFile = $this->getAuthPath();\n\n        $config = json_decode(file_get_contents($authFile), true);\n\n        return $config[$type][$hostname] ?? null;\n    }\n\n    /**\n     * makeComposer returns a new instance of composer\n     */\n    protected function makeComposer(): Composer\n    {\n        $composer = Factory::create($this->output);\n\n        // Disable scripts\n        $composer->getEventDispatcher()->setRunScripts(false);\n\n        // Discard changes to prevent corrupt state\n        $composer->getConfig()->merge([\n            'config' => [\n                'discard-changes' => true\n            ]\n        ]);\n\n        return $composer;\n    }\n\n    /**\n     * listPackagesInternal returns a list of installed packages\n     */\n    protected function listPackagesInternal($useDirect = true)\n    {\n        $composerLock = base_path('vendor/composer/installed.json');\n        $composerFile = $this->getJsonPath();\n\n        $installedPackages = json_decode(file_get_contents($composerLock), true);\n        $packages = $installedPackages['packages'] ?? [];\n\n        $filter = [];\n        if ($useDirect) {\n            $composerPackages = json_decode(file_get_contents($composerFile), true);\n            $require = array_merge(\n                $composerPackages['require'] ?? [],\n                $composerPackages['require-dev'] ?? []\n            );\n\n            foreach ($require as $pkg => $ver) {\n                $filter[$pkg] = true;\n            }\n        }\n\n        $result = [];\n        foreach ($packages as $package) {\n            $name = $package['name'] ?? '';\n            if ($useDirect && !isset($filter[$name])) {\n                continue;\n            }\n\n            $result[] = [\n                'name' => $name,\n                'version' => $this->normalizeVersion($package['version'] ?? ''),\n                'description' => $package['description'] ?? '',\n            ];\n        }\n\n        return $result;\n    }\n\n    /**\n     * normalizeVersion\n     */\n    protected function normalizeVersion($packageVersion)\n    {\n        $version = (new VersionParser)->normalize($packageVersion);\n        $parts = explode('.', $version);\n\n        if (count($parts) === 4 && preg_match('{^0\\D?}', $parts[3])) {\n            unset($parts[3]);\n            $version = implode('.', $parts);\n        }\n\n        return $version;\n    }\n\n    /**\n     * getJsonPath returns a path to the composer.json file\n     */\n    protected function getJsonPath(): string\n    {\n        return base_path('composer.json');\n    }\n\n    /**\n     * getAuthPath returns a path to the auth.json file\n     */\n    protected function getAuthPath(): string\n    {\n        return base_path('auth.json');\n    }\n}\n"
  },
  {
    "path": "src/Composer/Concerns/HasAssertions.php",
    "content": "<?php namespace October\\Rain\\Composer\\Concerns;\n\nuse Composer\\Util\\Platform;\nuse RecursiveIteratorIterator;\nuse RecursiveDirectoryIterator;\nuse DirectoryIterator;\nuse RegexIterator;\n\n/**\n * HasAssertions for composer\n *\n * @package october\\composer\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasAssertions\n{\n    /**\n     * @var string workingDir\n     */\n    protected $workingDir;\n\n    /**\n     * assertEnvironmentReady\n     */\n    protected function assertEnvironmentReady()\n    {\n        // Address resource limits\n        @set_time_limit(3600);\n        @ini_set('memory_limit', '-1');\n        ini_set('max_input_time', 0);\n        ini_set('max_execution_time', 0);\n\n        // Fixes function throwing an error\n        require_once __DIR__ . '/../resources/file_get_contents.php';\n\n        // Function may be disabled for security reasons\n        if (!function_exists('putenv')) {\n            require_once __DIR__ . '/../resources/putenv.php';\n        }\n    }\n\n    /**\n     * assertHomeVariableSet\n     */\n    protected function assertHomeVariableSet()\n    {\n        // Something usable is already set\n        $osHome = Platform::isWindows() ? 'APPDATA' : 'HOME';\n        if (Platform::getEnv('COMPOSER_HOME') || Platform::getEnv($osHome)) {\n            return;\n        }\n\n        // Prepare a home location for composer\n        $tempPath = temp_path('composer');\n        if (!file_exists($tempPath)) {\n            @mkdir($tempPath);\n        }\n\n        Platform::putEnv('COMPOSER_HOME', $tempPath);\n    }\n\n    /**\n     * assertHomeDirectory\n     */\n    protected function assertHomeDirectory()\n    {\n        $this->workingDir = getcwd();\n        chdir(dirname($this->getJsonPath()));\n    }\n\n    /**\n     * assertWorkingDirectory\n     */\n    protected function assertWorkingDirectory()\n    {\n        chdir($this->workingDir);\n    }\n\n    /**\n     * assertComposerWarmedUp preloads composer in case it wants to update itself\n     */\n    protected function assertComposerWarmedUp()\n    {\n        // Preload root package\n        $this->assertPackageLoaded('Composer', base_path('vendor/composer/composer/src/Composer'), false);\n\n        // Preload child packages\n        $preload = [\n            'Composer\\Autoload',\n            'Composer\\Config',\n            'Composer\\DependencyResolver',\n            'Composer\\Downloader',\n            'Composer\\EventDispatcher',\n            'Composer\\Exception',\n            'Composer\\Filter',\n            'Composer\\Installer',\n            'Composer\\IO',\n            'Composer\\Json',\n            'Composer\\Package',\n            'Composer\\Platform',\n            'Composer\\Plugin',\n            'Composer\\Question',\n            'Composer\\Repository',\n            'Composer\\Script',\n            'Composer\\SelfUpdate',\n            'Composer\\Util',\n        ];\n\n        foreach ($preload as $package) {\n            $this->assertPackageLoaded(\n                $package,\n                base_path('vendor/composer/composer/src/'.str_replace(\"\\\\\", \"/\", $package))\n            );\n        }\n    }\n\n    /**\n     * assertPackageLoaded ensures all classes in a package are loaded\n     */\n    protected function assertPackageLoaded($packageName, $packagePath, $recursive = true)\n    {\n        $allFiles = $recursive\n            ? new RecursiveIteratorIterator(new RecursiveDirectoryIterator($packagePath))\n            : new DirectoryIterator($packagePath);\n\n        $phpFiles = new RegexIterator($allFiles, '/\\.php$/');\n        $packagePathLen = strlen($packagePath);\n\n        foreach ($phpFiles as $phpFile) {\n            // Remove base directory and .php extension\n            $className = substr($phpFile->getRealPath(), $packagePathLen, -4);\n\n            // Normalize OS path separators, normalize to a class namespace\n            $className = trim(str_replace(\"/\", \"\\\\\", $className), '\\\\');\n\n            // Build complete namespace\n            $className = $packageName . '\\\\' . $className;\n\n            // Preload class\n            class_exists($className);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Composer/Concerns/HasAutoloader.php",
    "content": "<?php namespace October\\Rain\\Composer\\Concerns;\n\n/**\n * HasAutoloader for composer\n *\n * This trait manages composer packages introduced by plugins. Each loaded\n * package is added to a global pool to ensure a package is not loaded\n * twice by the composer instance introduced by a plugin. This class\n * is used as a substitute for the vendor/autoload.php file.\n *\n * @package october\\composer\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasAutoloader\n{\n    /**\n     * @var array namespacePool\n     */\n    protected $namespacePool = [];\n\n    /**\n     * @var array psr4Pool\n     */\n    protected $psr4Pool = [];\n\n    /**\n     * @var array classMapPool\n     */\n    protected $classMapPool = [];\n\n    /**\n     * @var array includeFilesPool\n     */\n    protected $includeFilesPool = [];\n\n    /**\n     * @var Composer\\Autoload\\ClassLoader loader for the primary composer instance.\n     */\n    protected $loader;\n\n    /**\n     * initAutoloader\n     */\n    protected function initAutoloader()\n    {\n        if ($this->loader === null && file_exists($autoLoadFile = base_path('vendor/autoload.php'))) {\n            $this->loader = include $autoLoadFile;\n            $this->preloadPools();\n        }\n    }\n\n    /**\n     * autoload is a similar function to including vendor/autoload.php.\n     * @param string $vendorPath Absoulte path to the vendor directory.\n     * @return void\n     */\n    public function autoload($vendorPath)\n    {\n        $this->initAutoloader();\n\n        $dir = $vendorPath . '/composer';\n\n        if (file_exists($file = $dir . '/autoload_namespaces.php')) {\n            $map = require $file;\n            foreach ($map as $namespace => $path) {\n                if (isset($this->namespacePool[$namespace])) {\n                    continue;\n                }\n                $this->loader->set($namespace, $path);\n                $this->namespacePool[$namespace] = true;\n            }\n        }\n\n        if (file_exists($file = $dir . '/autoload_psr4.php')) {\n            $map = require $file;\n            foreach ($map as $namespace => $path) {\n                if (isset($this->psr4Pool[$namespace])) {\n                    continue;\n                }\n                $this->loader->setPsr4($namespace, $path);\n                $this->psr4Pool[$namespace] = true;\n            }\n        }\n\n        if (file_exists($file = $dir . '/autoload_classmap.php')) {\n            $classMap = require $file;\n            if ($classMap) {\n                $classMapDiff = array_diff_key($classMap, $this->classMapPool);\n                $this->loader->addClassMap($classMapDiff);\n                $this->classMapPool += array_fill_keys(array_keys($classMapDiff), true);\n            }\n        }\n\n        if (file_exists($file = $dir . '/autoload_files.php')) {\n            $includeFiles = require $file;\n            foreach ($includeFiles as $includeFile) {\n                $relativeFile = $this->stripVendorDir($includeFile, $vendorPath);\n                if (isset($this->includeFilesPool[$relativeFile])) {\n                    continue;\n                }\n                require $includeFile;\n                $this->includeFilesPool[$relativeFile] = true;\n            }\n        }\n    }\n\n    /**\n     * preloadPools\n     */\n    protected function preloadPools()\n    {\n        $this->classMapPool = array_fill_keys(array_keys($this->loader->getClassMap()), true);\n        $this->namespacePool = array_fill_keys(array_keys($this->loader->getPrefixes()), true);\n        $this->psr4Pool = array_fill_keys(array_keys($this->loader->getPrefixesPsr4()), true);\n        $this->includeFilesPool = $this->preloadIncludeFilesPool();\n    }\n\n    /**\n     * preloadIncludeFilesPool\n     */\n    protected function preloadIncludeFilesPool()\n    {\n        $result = [];\n        $vendorPath = base_path() .'/vendor';\n\n        if (file_exists($file = $vendorPath . '/composer/autoload_files.php')) {\n            $includeFiles = require $file;\n            foreach ($includeFiles as $includeFile) {\n                $relativeFile = $this->stripVendorDir($includeFile, $vendorPath);\n                $result[$relativeFile] = true;\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * stripVendorDir removes the vendor directory from a path.\n     * @param string $path\n     * @return string\n     */\n    protected function stripVendorDir($path, $vendorDir)\n    {\n        $path = realpath($path);\n        $vendorDir = realpath($vendorDir);\n\n        if (strpos($path, $vendorDir) === 0) {\n            $path = substr($path, strlen($vendorDir));\n        }\n\n        return $path;\n    }\n}\n"
  },
  {
    "path": "src/Composer/Concerns/HasOctoberCommands.php",
    "content": "<?php namespace October\\Rain\\Composer\\Concerns;\n\n/**\n * HasOctoberCommands for composer\n *\n * @package october\\composer\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasOctoberCommands\n{\n    /**\n     * addOctoberRepository\n     */\n    public function addOctoberRepository(string $url)\n    {\n        $this->addRepository(\n            'octobercms',\n            'composer',\n            $url,\n            [\n                'only' => ['october/*', '*-plugin', '*-theme']\n            ]\n        );\n    }\n}\n"
  },
  {
    "path": "src/Composer/Concerns/HasOutput.php",
    "content": "<?php namespace October\\Rain\\Composer\\Concerns;\n\nuse Composer\\IO\\NullIO;\nuse Composer\\IO\\BufferIO;\nuse Composer\\IO\\ConsoleIO;\nuse Composer\\IO\\IOInterface;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Command\\Command;\n\n/**\n * HasOutput for composer\n *\n * @package october\\composer\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasOutput\n{\n    /**\n     * @var IOInterface output\n     */\n    protected $output;\n\n    /**\n     * setOutput\n     */\n    public function setOutput(?IOInterface $output = null)\n    {\n        if ($output === null) {\n            $this->output = new NullIO();\n        }\n        else {\n            $this->output = $output;\n        }\n    }\n\n    /**\n     * setOutputCommand\n     */\n    public function setOutputCommand(Command $command, InputInterface $input)\n    {\n        $this->setOutput(new ConsoleIO($input, $command->getOutput(), $command->getHelperSet()));\n    }\n\n    /**\n     * setOutputBuffer\n     */\n    public function setOutputBuffer()\n    {\n        $this->setOutput(new BufferIO());\n    }\n\n    /**\n     * getOutputBuffer\n     */\n    public function getOutputBuffer(): string\n    {\n        if ($this->output instanceof BufferIO) {\n            return $this->output->getOutput();\n        }\n\n        return '';\n    }\n}\n"
  },
  {
    "path": "src/Composer/Concerns/HasRequirements.php",
    "content": "<?php namespace October\\Rain\\Composer\\Concerns;\n\nuse Composer\\Json\\JsonFile;\nuse Composer\\Json\\JsonManipulator;\n\n/**\n * HasRequirements for composer\n *\n * @package october\\composer\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasRequirements\n{\n    /**\n     * @var string composerBackup contents\n     */\n    protected $composerBackup;\n\n    /**\n     * writePackages stores package changes to disk. The requirements key is the package name and the value\n     * is the version constraint or false to remove the requirement.\n     */\n    protected function writePackages(array $requirements)\n    {\n        $sortPackages = false;\n        $isDev = false;\n        $requireKey = $isDev ? 'require-dev' : 'require';\n        $removeKey = $isDev ? 'require' : 'require-dev';\n        $json = new JsonFile($this->getJsonPath());\n        $result = null;\n\n        // Update cleanly\n        $contents = file_get_contents($json->getPath());\n        $manipulator = new JsonManipulator($contents);\n\n        foreach ($requirements as $package => $version) {\n            if ($version !== false) {\n                $result = $manipulator->addLink($requireKey, $package, $version, $sortPackages);\n            }\n            else {\n                $result = $manipulator->removeSubNode($requireKey, $package);\n            }\n\n            if ($result) {\n                $result = $manipulator->removeSubNode($removeKey, $package);\n            }\n        }\n\n        if ($result) {\n            $manipulator->removeMainKeyIfEmpty($removeKey);\n            file_put_contents($json->getPath(), $manipulator->getContents());\n            return;\n        }\n\n        // Fallback update\n        $composerDefinition = $json->read();\n        foreach ($requirements as $package => $version) {\n            if ($version !== false) {\n                $composerDefinition[$requireKey][$package] = $version;\n            }\n            else {\n                unset($composerDefinition[$requireKey][$package]);\n            }\n\n            unset($composerDefinition[$removeKey][$package]);\n\n            if (isset($composerDefinition[$removeKey]) && count($composerDefinition[$removeKey]) === 0) {\n                unset($composerDefinition[$removeKey]);\n            }\n        }\n\n        $json->write($composerDefinition);\n    }\n\n    /**\n     * restoreComposerFile\n     */\n    protected function restoreComposerFile()\n    {\n        if ($this->composerBackup) {\n            file_put_contents($this->getJsonPath(), $this->composerBackup);\n        }\n    }\n\n    /**\n     * backupComposerFile\n     */\n    protected function backupComposerFile()\n    {\n        $this->composerBackup = file_get_contents($this->getJsonPath());\n    }\n}\n"
  },
  {
    "path": "src/Composer/resources/file_get_contents.php",
    "content": "<?php\n\n/**\n * This prevents file_get_contents from throwing an exception.\n */\nnamespace Composer\\Repository\n{\n    /**\n     * file_get_contents is not suppressed in composer\n     */\n    function file_get_contents(...$args)\n    {\n        return @\\file_get_contents(...$args);\n    }\n}\n"
  },
  {
    "path": "src/Composer/resources/putenv.php",
    "content": "<?php\n\n/**\n * This file registers a null functions for specific packages where putenv()\n * is disabled since it is not necessary for web runtime execution.\n */\nnamespace Composer\\Util\n{\n    /**\n     * putenv cannot be removed so we suppress it\n     */\n    function putenv()\n    {\n        // Do nothing\n    }\n}\n"
  },
  {
    "path": "src/Config/FileLoader.php",
    "content": "<?php namespace October\\Rain\\Config;\n\nuse Symfony\\Component\\Finder\\Finder;\nuse SplFileInfo;\n\n/**\n * FileLoader loads package config\n *\n * @package october/config\n * @author Alexey Bobkov, Samuel Georges\n */\nclass FileLoader\n{\n    /**\n     * fromPath returns config files in a given path\n     */\n    public static function fromPath($path)\n    {\n        return self::getConfigurationFiles($path);\n    }\n\n    /**\n     * Get all of the configuration files for the application.\n     *\n     * @param  \\Illuminate\\Contracts\\Foundation\\Application  $app\n     * @return array\n     */\n    protected static function getConfigurationFiles(string $path)\n    {\n        $files = [];\n\n        $configPath = realpath($path);\n        if (!$configPath) {\n            return $files;\n        }\n\n        foreach (Finder::create()->files()->name('*.php')->in($configPath) as $file) {\n            $directory = self::getNestedDirectory($file, $configPath);\n\n            $files[$directory.basename($file->getRealPath(), '.php')] = $file->getRealPath();\n        }\n\n        ksort($files, SORT_NATURAL);\n\n        return $files;\n    }\n\n    /**\n     * Get the configuration file nesting path.\n     *\n     * @param  \\SplFileInfo  $file\n     * @param  string  $configPath\n     * @return string\n     */\n    protected static function getNestedDirectory(SplFileInfo $file, $configPath)\n    {\n        $directory = $file->getPath();\n\n        if ($nested = trim(str_replace($configPath, '', $directory), DIRECTORY_SEPARATOR)) {\n            $nested = str_replace(DIRECTORY_SEPARATOR, '.', $nested).'.';\n        }\n\n        return $nested;\n    }\n}\n"
  },
  {
    "path": "src/Config/README.md",
    "content": "Config\n=======\n\nAn extension of illuminate\\config\n\nModules and plugins can have config files in the /config directory. Plugin and module configuration files are registered automatically.\n\n## Accessing configuration strings\n\n````\n// Get a configuration string from the CMS module\necho Config::get('cms::options.allow_comments');\n\n// Get a configuration string from the october/blog plugin.\necho Config::get('october.blog::options.allow_comments');\n````\n\n## Overriding configuration strings\n\nSystem users can override configuration strings without altering the modules' and plugins' files. This is done by adding configuration files to the app/config directory. To override a plugin's configuration:\n\n````\napp\n  config\n    vendorname\n      pluginname\n        file.php\n````\nExample: config/october/blog/options.php\n\nTo override a module's configuration:\n\n````\napp\n  config\n    modulename\n      file.php\n````\nExample: config/cms/options.php"
  },
  {
    "path": "src/Config/Repository.php",
    "content": "<?php namespace October\\Rain\\Config;\n\nuse Illuminate\\Config\\Repository as RepositoryBase;\nuse Arr;\n\n/**\n * Repository for configuration in October CMS\n *\n * @package october/config\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Repository extends RepositoryBase\n{\n    /**\n     * package registers a package configuration\n     * @param  string  $namespace\n     * @param  string  $path\n     * @return void\n     */\n    public function package($namespace, $path)\n    {\n        // Locate config files found in the package\n        foreach (FileLoader::fromPath($path) as $key => $filePath) {\n\n            // Filenames with config.php are treated as root nodes\n            $configKey = $key === 'config' ? $namespace : $namespace . '.' . $key;\n\n            // Core config overrides package config\n            $coreConfig = $this->get($configKey, []);\n            $baseConfig = require $filePath;\n            $this->set($configKey, $coreConfig + $baseConfig);\n        }\n    }\n\n    /**\n     * has determines if the given configuration value exists.\n     *\n     * @param  string  $key\n     * @return bool\n     */\n    public function has($key)\n    {\n        return Arr::has($this->items, $this->toNsKey($key));\n    }\n\n    /**\n     * get the specified configuration value.\n     *\n     * @param  array|string  $key\n     * @param  mixed  $default\n     * @return mixed\n     */\n    public function get($key, $default = null)\n    {\n        if (is_array($key)) {\n            return $this->getMany($key);\n        }\n\n        return Arr::get($this->items, $this->toNsKey($key), $default);\n    }\n\n    /**\n     * getMany configuration values.\n     *\n     * @param  array  $keys\n     * @return array\n     */\n    public function getMany($keys)\n    {\n        $config = [];\n\n        foreach ($keys as $key => $default) {\n            if (is_numeric($key)) {\n                [$key, $default] = [$default, null];\n            }\n\n            $newKey = $this->toNsKey($key);\n            $config[$newKey] = Arr::get($this->items, $newKey, $default);\n        }\n\n        return $config;\n    }\n\n    /**\n     * set a given configuration value.\n     *\n     * @param  array|string  $key\n     * @param  mixed  $value\n     * @return void\n     */\n    public function set($key, $value = null)\n    {\n        $keys = is_array($key) ? $key : [$key => $value];\n\n        foreach ($keys as $key => $value) {\n            Arr::set($this->items, $this->toNsKey($key), $value);\n        }\n    }\n\n    /**\n     * toNsKey converts a namespaced key to an array key\n     */\n    protected function toNsKey($key)\n    {\n        if (strpos($key, '::') !== false) {\n            return str_replace(['::config', '::'], ['', '.'], $key);\n        }\n\n        return $key;\n    }\n}\n"
  },
  {
    "path": "src/Database/Attach/File.php",
    "content": "<?php namespace October\\Rain\\Database\\Attach;\n\nuse Log;\nuse Http;\nuse Cache;\nuse Storage;\nuse Response;\nuse File as FileHelper;\nuse Illuminate\\Support\\Arr;\nuse October\\Rain\\Database\\Model;\nuse October\\Rain\\Resize\\Resizer;\nuse Symfony\\Component\\HttpFoundation\\File\\UploadedFile;\nuse Symfony\\Component\\HttpFoundation\\File\\File as FileObj;\nuse Exception;\n\n/**\n * File attachment model\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass File extends Model\n{\n    use \\October\\Rain\\Database\\Traits\\Sortable;\n\n    /**\n     * @var string table associated with the model\n     */\n    protected $table = 'files';\n\n    /**\n     * @var array morphTo relation\n     */\n    public $morphTo = [\n        'attachment' => []\n    ];\n\n    /**\n     * @var array fillable attributes are mass assignable\n     */\n    protected $fillable = [\n        'file_name',\n        'title',\n        'description',\n        'field',\n        'attachment_id',\n        'attachment_type',\n        'is_public',\n        'sort_order',\n        'data',\n    ];\n\n    /**\n     * @var array guarded attributes aren't mass assignable\n     */\n    protected $guarded = [];\n\n    /**\n     * @var array imageExtensions known\n     */\n    public static $imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif'];\n\n    /**\n     * @var array hidden fields from array/json access\n     */\n    protected $hidden = ['attachment_type', 'attachment_id', 'is_public'];\n\n    /**\n     * @var array appends fields to array/json access\n     */\n    protected $appends = ['path', 'extension'];\n\n    /**\n     * @var mixed data is a local file name or an instance of an uploaded file,\n     * objects of the UploadedFile class.\n     */\n    public $data = null;\n\n    /**\n     * @var array autoMimeTypes\n     */\n    protected $autoMimeTypes = [\n        'docx' => 'application/msword',\n        'xlsx' => 'application/excel',\n        'gif'  => 'image/gif',\n        'png'  => 'image/png',\n        'jpg'  => 'image/jpeg',\n        'jpeg' => 'image/jpeg',\n        'webp' => 'image/webp',\n        'avif' => 'image/avif',\n        'pdf'  => 'application/pdf',\n        'svg'  => 'image/svg+xml',\n    ];\n\n    //\n    // Constructors\n    //\n\n    /**\n     * fromPost creates a file object from a file an uploaded file, the input can be an\n     * upload object or the input name from a file postback.\n     * @param string|UploadedFile $fileInput\n     * @return $this\n     */\n    public function fromPost($fileInput)\n    {\n        if (is_string($fileInput)) {\n            $fileInput = files($fileInput);\n        }\n\n        if (!$fileInput) {\n            return;\n        }\n\n        $this->file_name = $fileInput->getClientOriginalName();\n        $this->file_size = $fileInput->getSize();\n        $this->content_type = $fileInput->getMimeType();\n        $this->disk_name = $this->getDiskName();\n\n        // getRealPath() can be empty for some environments (IIS)\n        $realPath = empty(trim($fileInput->getRealPath()))\n            ? $fileInput->getPath() . DIRECTORY_SEPARATOR . $fileInput->getFileName()\n            : $fileInput->getRealPath();\n\n        $this->putFile($realPath, $this->disk_name);\n\n        return $this;\n    }\n\n    /**\n     * fromFile creates a file object from a file on the disk\n     * @param string $filePath\n     * @param string $filename\n     * @return $this\n     */\n    public function fromFile($filePath, $filename = null)\n    {\n        if ($filePath === null) {\n            return;\n        }\n\n        $file = new FileObj($filePath);\n        $this->file_name = empty($filename) ? $file->getFilename() : $filename;\n        $this->file_size = $file->getSize();\n        $this->content_type = $file->getMimeType();\n        $this->disk_name = $this->getDiskName();\n\n        $this->putFile($file->getRealPath(), $this->disk_name);\n\n        return $this;\n    }\n\n    /**\n     * fromData creates a file object from raw data\n     * @param string $data\n     * @param string $filename\n     */\n    public function fromData($data, $filename)\n    {\n        if ($data === null) {\n            return;\n        }\n\n        $tempName = str_replace('.', '', uniqid('', true)) . '.tmp';\n        $tempPath = temp_path($tempName);\n        FileHelper::put($tempPath, $data);\n\n        $file = $this->fromFile($tempPath, basename($filename));\n        FileHelper::delete($tempPath);\n\n        return $file;\n    }\n\n    /**\n     * fromUrl creates a file object from url\n     * @param string $url\n     * @param string $filename\n     * @return self\n     */\n    public function fromUrl($url, $filename = null)\n    {\n        $data = Http::get($url);\n\n        if ($data->status() !== 200) {\n            throw new Exception(sprintf('Error getting file \"%s\", error code: %d', $url, $data->status()));\n        }\n\n        if (empty($filename)) {\n            $filename = basename(parse_url($url, PHP_URL_PATH));\n        }\n\n        return $this->fromData($data->body(), $filename);\n    }\n\n    //\n    // Attribute mutators\n    //\n\n    /**\n     * getUrlAttribute helper attribute for getUrl\n     * @return string\n     */\n    public function getUrlAttribute()\n    {\n        return $this->getUrl();\n    }\n\n    /**\n     * @deprecated see getUrlAttribute\n     */\n    public function getPathAttribute()\n    {\n        return $this->getPath();\n    }\n\n    /**\n     * getExtensionAttribute helper attribute for getExtension\n     * @return string\n     */\n    public function getExtensionAttribute()\n    {\n        return $this->getExtension();\n    }\n\n    /**\n     * setDataAttribute used only when filling attributes\n     */\n    public function setDataAttribute($value)\n    {\n        $this->data = $value;\n    }\n\n    /**\n     * getWidthAttribute helper attribute for get image width\n     * @return string|null\n     */\n    public function getWidthAttribute()\n    {\n        if (!$this->isImage()) {\n            return null;\n        }\n\n        $dimensions = $this->getImageDimensions();\n        if (!$dimensions) {\n            return null;\n        }\n\n        return $dimensions[0];\n    }\n\n    /**\n     * getHeightAttribute helper attribute for get image height\n     * @return string|null\n     */\n    public function getHeightAttribute()\n    {\n        if (!$this->isImage()) {\n            return null;\n        }\n\n        $dimensions = $this->getImageDimensions();\n        if (!$dimensions) {\n            return null;\n        }\n\n        return $dimensions[1];\n    }\n\n    /**\n     * getSizeAttribute helper attribute for file size in human format\n     * @return string\n     */\n    public function getSizeAttribute()\n    {\n        return $this->sizeToString();\n    }\n\n    //\n    // Output and Download\n    //\n\n    /**\n     * download the file contents\n     * @return Response\n     */\n    public function download()\n    {\n        return Response::download($this->getLocalPath(), $this->file_name);\n    }\n\n    /**\n     * output the raw file contents\n     * @param string $disposition see the download method @deprecated\n     * @param bool $returnResponse Direct output will be removed soon, chain with ->send() @deprecated\n     * @return Response\n     */\n    public function output($disposition = 'inline', $returnResponse = true)\n    {\n        if ($disposition === 'attachment') {\n            return $this->download();\n        }\n\n        $response = Response::file($this->getLocalPath());\n\n        if ($returnResponse) {\n            return $response;\n        }\n\n        $response->send();\n    }\n\n    /**\n     * outputThumb the raw thumb file contents\n     * @param integer $width\n     * @param integer $height\n     * @param array $options [\n     *     'mode' => 'auto',\n     *     'offset' => [0, 0],\n     *     'quality' => 90,\n     *     'sharpen' => 0,\n     *     'interlace' => false,\n     *     'extension' => 'auto',\n     *     'disposition' => 'inline',\n     * ]\n     * @param bool $returnResponse Direct output will be removed soon, chain with ->send() @deprecated\n     * @todo Refactor thumb to resources and recommend it be local, if remote, still use content grabber\n     * @return Response|void\n     */\n    public function outputThumb($width, $height, $options = [], $returnResponse = true)\n    {\n        $disposition = Arr::get($options, 'disposition', 'inline');\n        $options = $this->getDefaultThumbOptions($options);\n\n        // Generate thumb if not existing already\n        $thumbFile = $this->getThumbFilename($width, $height, $options);\n        if (\n            !$this->hasFile($thumbFile) &&\n            !$this->getThumb($width, $height, $options)\n        ) {\n            throw new Exception(sprintf('Thumb file \"%s\" failed to generate. Check error logs for more details.', $thumbFile));\n        }\n\n        $contents = $this->getContents($thumbFile);\n\n        $response = Response::make($contents)->withHeaders([\n            'Content-type' => $this->getContentType(),\n            'Content-Disposition' => $disposition . '; filename=\"' . basename($thumbFile) . '\"',\n            'Cache-Control' => 'private, no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0',\n            'Accept-Ranges' => 'bytes',\n            'Content-Length' => mb_strlen($contents, '8bit'),\n        ]);\n\n        if ($returnResponse) {\n            return $response;\n        }\n\n        $response->send();\n    }\n\n    //\n    // Getters\n    //\n\n    /**\n     * getCacheKey returns the cache key used for the hasFile method\n     * @param string $path The path to get the cache key for\n     * @return string\n     */\n    public function getCacheKey($path = null)\n    {\n        if (empty($path)) {\n            $path = $this->getDiskPath();\n        }\n\n        return 'database-file::' . $path;\n    }\n\n    /**\n     * getFilename returns the file name without path\n     */\n    public function getFilename()\n    {\n        return $this->file_name;\n    }\n\n    /**\n     * getExtension returns the file extension\n     */\n    public function getExtension()\n    {\n        return FileHelper::extension($this->file_name);\n    }\n\n    /**\n     * getLastModified returns the last modification date as a UNIX timestamp\n     * @return int\n     */\n    public function getLastModified($fileName = null)\n    {\n        return $this->storageCmd('lastModified', $this->getDiskPath($fileName));\n    }\n\n    /**\n     * getContentType returns the file content type\n     */\n    public function getContentType()\n    {\n        if ($this->content_type !== null) {\n            return $this->content_type;\n        }\n\n        $ext = $this->getExtension();\n        if (isset($this->autoMimeTypes[$ext])) {\n            return $this->content_type = $this->autoMimeTypes[$ext];\n        }\n\n        return null;\n    }\n\n    /**\n     * getContents from storage device\n     */\n    public function getContents($fileName = null)\n    {\n        return $this->storageCmd('get', $this->getDiskPath($fileName));\n    }\n\n    /**\n     * getUrl returns a URL for this attachment\n     */\n    public function getUrl()\n    {\n        return $this->getPath();\n    }\n\n    /**\n     * getPath returns the URL path to access this file or a thumb file\n     */\n    public function getPath($fileName = null)\n    {\n        if (empty($fileName)) {\n            $fileName = $this->disk_name;\n        }\n\n        return $this->getPublicPath() . $this->getPartitionDirectory() . $fileName;\n    }\n\n    /**\n     * getLocalPath returns a local path to this file. If the file is stored remotely,\n     * it will be downloaded to a temporary directory.\n     */\n    public function getLocalPath()\n    {\n        if ($this->isLocalStorage()) {\n            return $this->getLocalRootPath() . '/' . $this->getDiskPath();\n        }\n\n        $itemSignature = md5($this->getPath()) . $this->getLastModified();\n\n        $cachePath = $this->getLocalTempPath($itemSignature . '.' . $this->getExtension());\n\n        if (!FileHelper::exists($cachePath)) {\n            $this->copyStorageToLocal($this->getDiskPath(), $cachePath);\n        }\n\n        return $cachePath;\n    }\n\n    /**\n     * getDiskPath returns the path to the file, relative to the storage disk\n     * @return string\n     */\n    public function getDiskPath($fileName = null)\n    {\n        if (empty($fileName)) {\n            $fileName = $this->disk_name;\n        }\n\n        return $this->getStorageDirectory() . $this->getPartitionDirectory() . $fileName;\n    }\n\n    /**\n     * isPublic determines if the file is flagged \"public\" or not\n     */\n    public function isPublic()\n    {\n        if (array_key_exists('is_public', $this->attributes)) {\n            return $this->attributes['is_public'];\n        }\n\n        if (isset($this->is_public)) {\n            return $this->is_public;\n        }\n\n        return true;\n    }\n\n    /**\n     * sizeToString returns the file size as string\n     * @return string Returns the size as string.\n     */\n    public function sizeToString()\n    {\n        return FileHelper::sizeToString($this->file_size);\n    }\n\n    //\n    // Events\n    //\n\n    /**\n     * beforeSave check if new file data has been supplied\n     * eg: $model->data = files('something');\n     */\n    public function beforeSave()\n    {\n        // Process the data property\n        if ($this->data !== null) {\n            if ($this->data instanceof UploadedFile) {\n                $this->fromPost($this->data);\n            }\n\n            $this->data = null;\n        }\n    }\n\n    /**\n     * afterDelete clean up it's thumbnails\n     */\n    public function afterDelete()\n    {\n        try {\n            if ($this->shouldDeleteFile()) {\n                $this->deleteThumbs();\n                $this->deleteFile();\n            }\n        }\n        catch (Exception $ex) {\n        }\n    }\n\n    //\n    // Image Handling\n    //\n\n    /**\n     * isImage checks if the file extension is an image and returns true or false\n     */\n    public function isImage()\n    {\n        return in_array(strtolower($this->getExtension()), static::$imageExtensions);\n    }\n\n    /**\n     * getImageDimensions\n     * @return array|bool\n     */\n    protected function getImageDimensions()\n    {\n        return getimagesize($this->getLocalPath());\n    }\n\n    /**\n     * getThumbUrl generates and returns a thumbnail URL path\n     *\n     * @param integer $width\n     * @param integer $height\n     * @param array $options [\n     *     'mode' => 'auto',\n     *     'offset' => [0, 0],\n     *     'quality' => 90,\n     *     'sharpen' => 0,\n     *     'interlace' => false,\n     *     'extension' => 'auto',\n     * ]\n     * @return string\n     */\n    public function getThumbUrl($width, $height, $options = [])\n    {\n        if (!$this->isImage() || !$this->hasFile($this->disk_name)) {\n            return $this->getUrl();\n        }\n\n        $width = (int) $width;\n        $height = (int) $height;\n\n        $options = $this->getDefaultThumbOptions($options);\n\n        $thumbFile = $this->getThumbFilename($width, $height, $options);\n        $thumbPath = $this->getDiskPath($thumbFile);\n        $thumbPublic = $this->getPath($thumbFile);\n\n        if (!$this->hasFile($thumbFile)) {\n            try {\n                if ($this->isLocalStorage()) {\n                    $this->makeThumbLocal($thumbFile, $thumbPath, $width, $height, $options);\n                }\n                else {\n                    $this->makeThumbStorage($thumbFile, $thumbPath, $width, $height, $options);\n                }\n            }\n            catch (Exception $ex) {\n                Log::error($ex);\n                return '';\n            }\n        }\n\n        return $thumbPublic;\n    }\n\n    /**\n     * getThumb is shorter syntax for getThumbUrl\n     * @return string\n     */\n    public function getThumb($width, $height, $options = [])\n    {\n        return $this->getThumbUrl($width, $height, $options);\n    }\n\n    /**\n     * getThumbFilename generates a thumbnail filename\n     * @return string\n     */\n    public function getThumbFilename($width, $height, $options)\n    {\n        $options = $this->getDefaultThumbOptions($options);\n        $mode = $options['mode'];\n        $extension = $options['extension'];\n\n        if (is_array($options['offset'] ?? null)) {\n            $offsetX = $options['offset'][0] ?? ($options['offset']['x'] ?? 0);\n            $offsetY = $options['offset'][1] ?? ($options['offset']['y'] ?? 0);\n            return \"thumb_{$this->id}_{$width}_{$height}_{$offsetX}_{$offsetY}_{$mode}.{$extension}\";\n        }\n        else {\n            return \"thumb_{$this->id}_{$width}_{$height}_{$mode}.{$extension}\";\n        }\n    }\n\n    /**\n     * getDefaultThumbOptions returns the default thumbnail options\n     * @return array\n     */\n    protected function getDefaultThumbOptions($overrideOptions = [])\n    {\n        $defaultOptions = [\n            'mode' => 'auto',\n            'offset' => null,\n            'quality' => 90,\n            'sharpen' => 0,\n            'interlace' => false,\n            'extension' => 'auto',\n        ];\n\n        if (!is_array($overrideOptions)) {\n            $overrideOptions = ['mode' => $overrideOptions];\n        }\n\n        $options = array_merge($defaultOptions, $overrideOptions);\n\n        $options['mode'] = strtolower($options['mode']);\n\n        if (strtolower($options['extension']) === 'auto') {\n            $options['extension'] = strtolower($this->getExtension());\n        }\n\n        return $options;\n    }\n\n    /**\n     * makeThumbLocal generates the thumbnail based on the local file system. This step\n     * is necessary to simplify things and ensure the correct file permissions are given\n     * to the local files.\n     */\n    protected function makeThumbLocal($thumbFile, $thumbPath, $width, $height, $options)\n    {\n        $rootPath = $this->getLocalRootPath();\n        $filePath = $rootPath.'/'.$this->getDiskPath();\n        $thumbPath = $rootPath.'/'.$thumbPath;\n\n        // Generate thumbnail\n        Resizer::open($filePath)\n            ->resize($width, $height, $options)\n            ->save($thumbPath)\n        ;\n\n        FileHelper::chmod($thumbPath);\n    }\n\n    /**\n     * makeThumbStorage generates the thumbnail based on a remote storage engine\n     */\n    protected function makeThumbStorage($thumbFile, $thumbPath, $width, $height, $options)\n    {\n        $tempFile = $this->getLocalTempPath();\n        $tempThumb = $this->getLocalTempPath($thumbFile);\n\n        // Generate thumbnail\n        $this->copyStorageToLocal($this->getDiskPath(), $tempFile);\n\n        try {\n            Resizer::open($tempFile)\n                ->resize($width, $height, $options)\n                ->save($tempThumb)\n            ;\n        }\n        finally {\n            FileHelper::delete($tempFile);\n        }\n\n        // Publish to storage\n        $success = $this->copyLocalToStorage($tempThumb, $thumbPath);\n\n        // Clean up\n        FileHelper::delete($tempThumb);\n\n        // Eagerly cache remote exists call\n        if ($success) {\n            Cache::forever($this->getCacheKey($thumbPath), true);\n        }\n    }\n\n    /**\n     * deleteThumbs deletes all thumbnails for this file\n     */\n    public function deleteThumbs()\n    {\n        $pattern = 'thumb_'.$this->id.'_';\n\n        $directory = $this->getStorageDirectory() . $this->getPartitionDirectory();\n        $allFiles = $this->storageCmd('files', $directory);\n        $collection = [];\n        foreach ($allFiles as $file) {\n            if (str_starts_with(basename($file), $pattern)) {\n                $collection[] = $file;\n            }\n        }\n\n        // Delete the collection of files\n        if (!empty($collection)) {\n            if ($this->isLocalStorage()) {\n                FileHelper::delete($collection);\n            }\n            else {\n                $this->getDisk()->delete($collection);\n\n                foreach ($collection as $filePath) {\n                    Cache::forget($this->getCacheKey($filePath));\n                }\n            }\n        }\n    }\n\n    //\n    // File handling\n    //\n\n    /**\n     * getDiskName generates a disk name from the supplied file name\n     */\n    protected function getDiskName()\n    {\n        if ($this->disk_name !== null) {\n            return $this->disk_name;\n        }\n\n        $ext = strtolower($this->getExtension());\n        $name = str_replace('.', '', uniqid('', true));\n\n        return $this->disk_name = !empty($ext) ? $name.'.'.$ext : $name;\n    }\n\n    /**\n     * getLocalTempPath returns a temporary local path to work from\n     */\n    protected function getLocalTempPath($path = null)\n    {\n        if (!$path) {\n            return $this->getTempPath() . '/' . md5($this->getDiskPath()) . '.' . $this->getExtension();\n        }\n\n        return $this->getTempPath() . '/' . $path;\n    }\n\n    /**\n     * putFile saves a file\n     * @param string $sourcePath An absolute local path to a file name to read from.\n     * @param string $destinationFileName A storage file name to save to.\n     */\n    protected function putFile($sourcePath, $destinationFileName = null)\n    {\n        if (!$destinationFileName) {\n            $destinationFileName = $this->disk_name;\n        }\n\n        $destinationPath = $this->getStorageDirectory() . $this->getPartitionDirectory();\n\n        if (!$this->isLocalStorage()) {\n            return $this->copyLocalToStorage($sourcePath, $destinationPath . $destinationFileName);\n        }\n\n        // Using local storage, tack on the root path and work locally\n        // this will ensure the correct permissions are used.\n        $destinationPath = $this->getLocalRootPath() . '/' . $destinationPath;\n\n        // Verify the directory exists, if not try to create it. If creation fails\n        // because the directory was created by a concurrent process then proceed,\n        // otherwise trigger the error.\n        if (\n            !FileHelper::isDirectory($destinationPath) &&\n            !FileHelper::makeDirectory($destinationPath, 0755, true, true) &&\n            !FileHelper::isDirectory($destinationPath)\n        ) {\n            if (($lastErr = error_get_last()) !== null) {\n                trigger_error($lastErr['message'], E_USER_WARNING);\n            }\n        }\n\n        return FileHelper::copy($sourcePath, $destinationPath . $destinationFileName);\n    }\n\n    /**\n     * shouldDeleteFile returns true if the file should be deleted.\n     */\n    protected function shouldDeleteFile($fileName = null): bool\n    {\n        if (!$fileName) {\n            $fileName = $this->disk_name;\n        }\n\n        if (!$fileName) {\n            return false;\n        }\n\n        return $this\n            ->newQueryWithoutScopes()\n            ->where('disk_name', $fileName)\n            ->count() === 0;\n    }\n\n    /**\n     * deleteFile contents from storage device\n     */\n    protected function deleteFile($fileName = null)\n    {\n        if (!$fileName) {\n            $fileName = $this->disk_name;\n        }\n\n        $directory = $this->getStorageDirectory() . $this->getPartitionDirectory();\n        $filePath = $directory . $fileName;\n\n        if ($this->storageCmd('exists', $filePath)) {\n            $this->storageCmd('delete', $filePath);\n        }\n\n        // Clear remote storage cache\n        if (!$this->isLocalStorage()) {\n            Cache::forget($this->getCacheKey($filePath));\n        }\n\n        $this->deleteEmptyDirectory($directory);\n    }\n\n    /**\n     * hasFile checks file exists on storage device\n     */\n    protected function hasFile($fileName = null)\n    {\n        $filePath = $this->getDiskPath($fileName);\n\n        if ($this->isLocalStorage()) {\n            return $this->storageCmd('exists', $filePath);\n        }\n\n        // Cache remote storage results for performance increase\n        $result = Cache::memo()->rememberForever($this->getCacheKey($filePath), function() use ($filePath) {\n            return $this->storageCmd('exists', $filePath);\n        });\n\n        return $result;\n    }\n\n    /**\n     * deleteEmptyDirectory checks if directory is empty then deletes it,\n     * three levels up to match the partition directory.\n     */\n    protected function deleteEmptyDirectory($dir = null)\n    {\n        if (!$this->isDirectoryEmpty($dir)) {\n            return;\n        }\n\n        $this->storageCmd('deleteDirectory', $dir);\n\n        $dir = dirname($dir);\n        if (!$this->isDirectoryEmpty($dir)) {\n            return;\n        }\n\n        $this->storageCmd('deleteDirectory', $dir);\n\n        $dir = dirname($dir);\n        if (!$this->isDirectoryEmpty($dir)) {\n            return;\n        }\n\n        $this->storageCmd('deleteDirectory', $dir);\n    }\n\n    /**\n     * isDirectoryEmpty returns true if a directory contains no files\n     */\n    protected function isDirectoryEmpty($dir)\n    {\n        if (!$dir) {\n            return null;\n        }\n\n        return count($this->storageCmd('allFiles', $dir)) === 0;\n    }\n\n    //\n    // Storage interface\n    //\n\n    /**\n     * storageCmd calls a method against File or Storage depending on local storage\n     * This allows local storage outside the storage/app folder and is\n     * also good for performance. For local storage, *every* argument\n     * is prefixed with the local root path. Props to Laravel for\n     * the unified interface.\n     */\n    protected function storageCmd()\n    {\n        $args = func_get_args();\n        $command = array_shift($args);\n        $result = null;\n\n        if ($this->isLocalStorage()) {\n            $interface = 'File';\n            $path = $this->getLocalRootPath();\n            $args = array_map(function ($value) use ($path) {\n                return $path . '/' . $value;\n            }, $args);\n\n            $result = forward_static_call_array([$interface, $command], $args);\n        }\n        else {\n            $result = call_user_func_array([$this->getDisk(), $command], $args);\n        }\n\n        return $result;\n    }\n\n    /**\n     * copyStorageToLocal file\n     */\n    protected function copyStorageToLocal($storagePath, $localPath)\n    {\n        return FileHelper::put($localPath, $this->getDisk()->readStream($storagePath));\n    }\n\n    /**\n     * copyLocalToStorage file\n     */\n    protected function copyLocalToStorage($localPath, $storagePath)\n    {\n        return $this->getDisk()->putFileAs(\n            dirname($storagePath),\n            $localPath,\n            basename($storagePath),\n            $this->isPublic() ? 'public' : 'private'\n        );\n    }\n\n    //\n    // Configuration\n    //\n\n    /**\n     * getMaxFilesize returns the maximum size of an uploaded file as configured in php.ini\n     * @return int The maximum size of an uploaded file in kilobytes\n     */\n    public static function getMaxFilesize()\n    {\n        return round(UploadedFile::getMaxFilesize() / 1024);\n    }\n\n    /**\n     * getStorageDirectory defines the internal storage path, override this method\n     */\n    public function getStorageDirectory()\n    {\n        if ($this->isPublic()) {\n            return 'public/';\n        }\n\n        return 'protected/';\n    }\n\n    /**\n     * getPublicPath defines the public address for the storage path\n     */\n    public function getPublicPath()\n    {\n        if ($this->isPublic()) {\n            return 'http://localhost/storage/uploads/public/';\n        }\n\n        return 'http://localhost/storage/uploads/protected/';\n    }\n\n    /**\n     * getTempPath defines the internal working path, override this method\n     */\n    public function getTempPath()\n    {\n        $path = temp_path() . '/uploads';\n\n        if (!FileHelper::isDirectory($path)) {\n            FileHelper::makeDirectory($path, 0755, true, true);\n        }\n\n        return $path;\n    }\n\n    /**\n     * getDisk returns the storage disk the file is stored on\n     * @return FilesystemAdapter\n     */\n    public function getDisk()\n    {\n        return Storage::disk();\n    }\n\n    /**\n     * isLocalStorage returns true if the storage engine is local\n     */\n    protected function isLocalStorage()\n    {\n        return Storage::getDefaultDriver() === 'local';\n    }\n\n    /**\n    * getPartitionDirectory generates a partition for the file\n    * return /ABC/DE1/234 for an name of ABCDE1234.\n    * @param Attachment $attachment\n    * @param string $styleName\n    * @return mixed\n    */\n    protected function getPartitionDirectory()\n    {\n        return implode('/', array_slice(str_split($this->disk_name, 3), 0, 3)) . '/';\n    }\n\n    /**\n     * getLocalRootPath if working with local storage, determine the absolute local path\n     */\n    protected function getLocalRootPath()\n    {\n        return storage_path('app/uploads');\n    }\n}\n"
  },
  {
    "path": "src/Database/Attach/FileException.php",
    "content": "<?php namespace October\\Rain\\Database\\Attach;\n\nuse Exception;\n\n/**\n * File Exception\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass FileException extends Exception\n{\n\n}\n"
  },
  {
    "path": "src/Database/Builder.php",
    "content": "<?php namespace October\\Rain\\Database;\n\nuse Illuminate\\Pagination\\Paginator;\nuse Illuminate\\Database\\Eloquent\\Builder as BuilderModel;\nuse October\\Rain\\Support\\Facades\\DbDongle;\nuse Closure;\n\n/**\n * Builder class for queries, extends the Eloquent builder class.\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Builder extends BuilderModel\n{\n    use \\October\\Rain\\Database\\Concerns\\HasNicerPagination;\n    use \\October\\Rain\\Database\\Concerns\\HasEagerLoadAttachRelation;\n\n    /**\n     * eagerLoadRelation eagerly load the relationship on a set of models, with support\n     * for attach relations.\n     * @param  array  $models\n     * @param  string  $name\n     * @param  \\Closure  $constraints\n     * @return array\n     */\n    protected function eagerLoadRelation(array $models, $name, Closure $constraints)\n    {\n        if ($result = $this->eagerLoadAttachRelation($models, $name, $constraints)) {\n            return $result;\n        }\n\n        return parent::eagerLoadRelation($models, $name, $constraints);\n    }\n\n    /**\n     * lists gets an array with the values of a given column.\n     * @param  string  $column\n     * @param  string|null  $key\n     * @return array\n     */\n    public function lists($column, $key = null)\n    {\n        return $this->pluck($column, $key)->all();\n    }\n\n    /**\n     * searchWhere performs a search on this query for term found in columns.\n     * @param  string $term  Search query\n     * @param  array $columns Table columns to search\n     * @param  string $mode  Search mode: all, any, exact.\n     * @return static\n     */\n    public function searchWhere($term, $columns = [], $mode = 'all')\n    {\n        return $this->searchWhereInternal($term, $columns, $mode, 'and');\n    }\n\n    /**\n     * orSearchWhere adds an \"or search where\" clause to the query.\n     * @param  string $term  Search query\n     * @param  array $columns Table columns to search\n     * @param  string $mode  Search mode: all, any, exact.\n     * @return static\n     */\n    public function orSearchWhere($term, $columns = [], $mode = 'all')\n    {\n        return $this->searchWhereInternal($term, $columns, $mode, 'or');\n    }\n\n    /**\n     * searchWhereRelation performs a search on a relationship query.\n     *\n     * @param  string $term  Search query\n     * @param  string  $relation\n     * @param  array $columns Table columns to search\n     * @param  string $mode  Search mode: all, any, exact.\n     * @return static\n     */\n    public function searchWhereRelation($term, $relation, $columns = [], $mode = 'all')\n    {\n        return $this->whereHas($relation, function ($query) use ($term, $columns, $mode) {\n            $query->searchWhere($term, $columns, $mode);\n        });\n    }\n\n    /**\n     * orSearchWhereRelation adds an \"or where\" clause to a search relationship query.\n     * @param  string $term  Search query\n     * @param  string  $relation\n     * @param  array $columns Table columns to search\n     * @param  string $mode  Search mode: all, any, exact.\n     * @return static\n     */\n    public function orSearchWhereRelation($term, $relation, $columns = [], $mode = 'all')\n    {\n        return $this->orWhereHas($relation, function ($query) use ($term, $columns, $mode) {\n            $query->searchWhere($term, $columns, $mode);\n        });\n    }\n\n    /**\n     * Internal method to apply a search constraint to the query.\n     * Mode can be any of these options:\n     * - all: result must contain all words\n     * - any: result can contain any word\n     * - exact: result must contain the exact phrase\n     */\n    protected function searchWhereInternal($term, $columns, $mode, $boolean)\n    {\n        if (!is_array($columns)) {\n            $columns = [$columns];\n        }\n\n        if (!$mode) {\n            $mode = 'all';\n        }\n\n        $grammar = $this->query->getGrammar();\n\n        if ($mode === 'exact') {\n            $this->where(function ($query) use ($columns, $term, $grammar) {\n                foreach ($columns as $field) {\n                    if (!strlen($term)) {\n                        continue;\n                    }\n                    $rawField = DbDongle::cast($grammar->wrap($field), 'TEXT');\n                    $fieldSql = $this->query->raw(sprintf(\"lower(%s)\", $rawField));\n                    $termSql = '%'.trim(mb_strtolower($term)).'%';\n                    $query->orWhere($fieldSql, 'LIKE', $termSql);\n                }\n            }, null, null, $boolean);\n        }\n        else {\n            $words = explode(' ', $term);\n            $wordBoolean = $mode === 'any' ? 'or' : 'and';\n\n            $this->where(function ($query) use ($columns, $words, $wordBoolean, $grammar) {\n                foreach ($columns as $field) {\n                    $query->orWhere(function ($query) use ($field, $words, $wordBoolean, $grammar) {\n                        foreach ($words as $word) {\n                            if (!strlen($word)) {\n                                continue;\n                            }\n                            $rawField = DbDongle::cast($grammar->wrap($field), 'TEXT');\n                            $fieldSql = $this->query->raw(sprintf(\"lower(%s)\", $rawField));\n                            $wordSql = '%'.trim(mb_strtolower($word)).'%';\n                            $query->where($fieldSql, 'LIKE', $wordSql, $wordBoolean);\n                        }\n                    });\n                }\n            }, null, null, $boolean);\n        }\n\n        return $this;\n    }\n\n    /**\n     * paginate the given query.\n     *\n     * @param  int  $perPage\n     * @param  array  $columns\n     * @param  string $pageName\n     * @param  int  $page\n     * @return \\Illuminate\\Contracts\\Pagination\\LengthAwarePaginator\n     */\n    public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null, $total = null)\n    {\n        // Legacy signature support\n        // paginate($perPage, $page, $columns, $pageName)\n        if (!is_array($columns)) {\n            $_currentPage = $columns;\n            $_columns = $pageName;\n            $_pageName = $page;\n\n            $columns = is_array($_columns) ? $_columns : ['*'];\n            $pageName = $_pageName !== null ? $_pageName : 'page';\n            $page = is_array($_currentPage) ? null : $_currentPage;\n        }\n\n        $page = $page ?: Paginator::resolveCurrentPage($pageName);\n\n        $total = value($total) ?? $this->toBase()->getCountForPagination();\n\n        $perPage = value($perPage, $total) ?: $this->model->getPerPage();\n\n        $results = $total\n            ? $this->forPage($page, $perPage)->get($columns)\n            : $this->model->newCollection();\n\n        return $this->paginator($results, $total, $perPage, $page, [\n            'path' => Paginator::resolveCurrentPath(),\n            'pageName' => $pageName\n        ]);\n    }\n\n    /**\n     * simplePaginate the given query into a simple paginator.\n     *\n     * @param  int  $perPage\n     * @param  array  $columns\n     * @param  string $pageName\n     * @param  int  $currentPage\n     * @return \\Illuminate\\Contracts\\Pagination\\Paginator\n     */\n    public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $page = null)\n    {\n        // Legacy signature support\n        // paginate($perPage, $currentPage, $columns, $pageName)\n        if (!is_array($columns)) {\n            $_currentPage = $columns;\n            $_columns = $pageName;\n            $_pageName = $page;\n\n            $columns = is_array($_columns) ? $_columns : ['*'];\n            $pageName = $_pageName !== null ? $_pageName : 'page';\n            $page = is_array($_currentPage) ? null : $_currentPage;\n        }\n\n        $page = $page ?: Paginator::resolveCurrentPage($pageName);\n\n        $perPage = $perPage ?: $this->model->getPerPage();\n\n        $this->skip(($page - 1) * $perPage)->take($perPage + 1);\n\n        return $this->simplePaginator($this->get($columns), $perPage, $page, [\n            'path' => Paginator::resolveCurrentPath(),\n            'pageName' => $pageName\n        ]);\n    }\n\n    /**\n     * Dynamically handle calls into the query instance.\n     * @param  string  $method\n     * @param  array   $parameters\n     * @return mixed\n     */\n    public function __call($method, $parameters)\n    {\n        if ($this->model->methodExists($scope = 'scope'.ucfirst($method))) {\n            return $this->callScope([$this->model, $scope], $parameters);\n        }\n\n        return parent::__call($method, $parameters);\n    }\n\n    /**\n     * addWhereExistsQuery modifies the Laravel version to strip ORDER BY from the query,\n     * which is redundant in this context, also forbidden by the SQL Server driver.\n     */\n    public function addWhereExistsQuery($query, $boolean = 'and', $not = false)\n    {\n        $query->reorder();\n\n        return parent::addWhereExistsQuery($query, $boolean, $not);\n    }\n}\n"
  },
  {
    "path": "src/Database/Collection.php",
    "content": "<?php namespace October\\Rain\\Database;\n\nuse Illuminate\\Database\\Eloquent\\Collection as CollectionBase;\n\n/**\n * Proxy class.\n */\nclass Collection extends CollectionBase\n{\n    /**\n     * Get an array with the values of a given key.\n     *\n     * @param  string  $value\n     * @param  string  $key\n     * @return array\n     */\n    public function lists($value, $key = null)\n    {\n        return $this->pluck($value, $key)->all();\n    }\n}\n"
  },
  {
    "path": "src/Database/Concerns/HasAttributes.php",
    "content": "<?php namespace October\\Rain\\Database\\Concerns;\n\nuse October\\Rain\\Support\\Str;\nuse Exception;\n\n/**\n * HasAttributes concern for a model\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasAttributes\n{\n    /**\n     * attributesToArray converts the model's attributes to an array.\n     * @return array\n     */\n    public function attributesToArray()\n    {\n        $attributes = $this->getArrayableAttributes();\n\n        // Before Event\n        foreach ($attributes as $key => $value) {\n            if (($eventValue = $this->fireEvent('model.beforeGetAttribute', [$key], true)) !== null) {\n                $attributes[$key] = $eventValue;\n            }\n        }\n\n        // Dates\n        $attributes = $this->addDateAttributesToArray($attributes);\n\n        // Mutate\n        $attributes = $this->addMutatedAttributesToArray(\n            $attributes, $mutatedAttributes = $this->getMutatedAttributes()\n        );\n\n        // Casts\n        $attributes = $this->addCastAttributesToArray(\n            $attributes, $mutatedAttributes\n        );\n\n        // Appends\n        foreach ($this->getArrayableAppends() as $key) {\n            $attributes[$key] = $this->mutateAttributeForArray($key, null);\n        }\n\n        // Jsonable\n        $attributes = $this->addJsonableAttributesToArray(\n            $attributes, $mutatedAttributes\n        );\n\n        // After Event\n        foreach ($attributes as $key => $value) {\n            if (($eventValue = $this->fireEvent('model.getAttribute', [$key, $value], true)) !== null) {\n                $attributes[$key] = $eventValue;\n            }\n        }\n\n        return $attributes;\n    }\n\n    /**\n     * getAttribute from the model.\n     * Overridden from {@link Eloquent} to implement recognition of the relation.\n     * @return mixed\n     */\n    public function getAttribute($key)\n    {\n        if (\n            array_key_exists($key, $this->attributes) ||\n            $this->hasGetMutator($key) ||\n            $this->hasAttributeMutator($key) ||\n            $this->isClassCastable($key)\n        ) {\n            return $this->getAttributeValue($key);\n        }\n\n        return $this->getRelationValue($key);\n    }\n\n    /**\n     * getRelationValue gets a relationship value from a method.\n     * Overridden from {@link Eloquent} to implement recognition of the relation\n     * using October Rain's property-based relation definitions.\n     * @param string $key\n     * @return mixed\n     */\n    public function getRelationValue($key)\n    {\n        if ($this->relationLoaded($key)) {\n            return $this->relations[$key];\n        }\n\n        if (!$this->hasRelation($key)) {\n            return;\n        }\n\n        if ($this->attemptToAutoloadRelation($key)) {\n            return $this->relations[$key];\n        }\n\n        if ($this->preventsLazyLoading) {\n            $this->handleLazyLoadingViolation($key);\n        }\n\n        return $this->getRelationshipFromMethod($key);\n    }\n\n    /**\n     * getAttributeValue gets a plain attribute (not a relationship).\n     * @param  string  $key\n     * @return mixed\n     */\n    public function getAttributeValue($key)\n    {\n        /**\n         * @event model.beforeGetAttribute\n         * Called before the model attribute is retrieved\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.beforeGetAttribute', function ((string) $key) use (\\October\\Rain\\Database\\Model $model) {\n         *         if ($key === 'not-for-you-to-look-at') {\n         *             return 'you are not allowed here';\n         *         }\n         *     });\n         *\n         */\n        if (($attr = $this->fireEvent('model.beforeGetAttribute', [$key], true)) !== null) {\n            return $attr;\n        }\n\n        $attr = parent::getAttributeValue($key);\n\n        // Return valid json (boolean, array) if valid, otherwise\n        // jsonable fields will return a string for invalid data.\n        if ($this->isJsonable($key) && !empty($attr)) {\n            $_attr = json_decode($attr, true);\n            if (json_last_error() === JSON_ERROR_NONE) {\n                $attr = $_attr;\n            }\n        }\n\n        /**\n         * @event model.getAttribute\n         * Called after the model attribute is retrieved\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.getAttribute', function ((string) $key, $value) use (\\October\\Rain\\Database\\Model $model) {\n         *         if ($key === 'not-for-you-to-look-at') {\n         *             return \"Totally not $value\";\n         *         }\n         *     });\n         *\n         */\n        if (($_attr = $this->fireEvent('model.getAttribute', [$key, $attr], true)) !== null) {\n            return $_attr;\n        }\n\n        return $attr;\n    }\n\n    /**\n     * hasGetMutator determines if a get mutator exists for an attribute.\n     * @param  string  $key\n     * @return bool\n     */\n    public function hasGetMutator($key)\n    {\n        return $this->methodExists('get'.Str::studly($key).'Attribute');\n    }\n\n    /**\n     * setAttribute sets a given attribute on the model.\n     * @param string $key\n     * @param mixed $value\n     * @return void\n     */\n    public function setAttribute($key, $value)\n    {\n        // Attempting to set attribute [null] on model.\n        if (empty($key)) {\n            throw new Exception('Cannot access empty model attribute.');\n        }\n\n        // Handle direct relation setting\n        if ($this->hasRelation($key) && !$this->hasSetMutator($key)) {\n            return $this->setRelationSimpleValue($key, $value);\n        }\n\n        /**\n         * @event model.beforeSetAttribute\n         * Called before the model attribute is set\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.beforeSetAttribute', function ((string) $key, $value) use (\\October\\Rain\\Database\\Model $model) {\n         *         if ($key === 'do-not-touch') {\n         *             return \"$value has been touched\";\n         *         }\n         *     });\n         *\n         */\n        if (($_value = $this->fireEvent('model.beforeSetAttribute', [$key, $value], true)) !== null) {\n            $value = $_value;\n        }\n\n        // Jsonable\n        if ($this->isJsonable($key) && (!empty($value) || is_array($value))) {\n            $value = json_encode($value, JSON_UNESCAPED_UNICODE);\n        }\n\n        // Trim strings\n        if ($this->trimStrings && is_string($value)) {\n            $value = trim($value);\n        }\n\n        $result = parent::setAttribute($key, $value);\n\n        /**\n         * @event model.setAttribute\n         * Called after the model attribute is set\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.setAttribute', function ((string) $key, $value) use (\\October\\Rain\\Database\\Model $model) {\n         *         if ($key === 'do-not-touch') {\n         *             \\Log::info(\"{$key} has been touched and set to {$value}!\")\n         *         }\n         *     });\n         *\n         */\n        $this->fireEvent('model.setAttribute', [$key, $value]);\n\n        return $result;\n    }\n\n    /**\n     * hasSetMutator determines if a set mutator exists for an attribute.\n     * @param  string  $key\n     * @return bool\n     */\n    public function hasSetMutator($key)\n    {\n        return $this->methodExists('set'.Str::studly($key).'Attribute');\n    }\n\n    /**\n     * addCasts adds attribute casts for the model.\n     *\n     * @param  array $attributes\n     * @return void\n     */\n    public function addCasts($attributes)\n    {\n        $this->casts = array_merge($this->casts, $attributes);\n    }\n\n    /**\n     * getDates returns the attributes that should be converted to dates.\n     * @return array\n     */\n    public function getDates()\n    {\n        if (!$this->usesTimestamps()) {\n            return $this->dates;\n        }\n\n        $defaults = [\n            $this->getCreatedAtColumn(),\n            $this->getUpdatedAtColumn(),\n        ];\n\n        return array_unique(array_merge($this->dates, $defaults));\n    }\n\n    /**\n     * addDateAttribute adds a datetime attribute to convert to an instance\n     * of Carbon/DateTime object.\n     * @param string   $attribute\n     * @return void\n     */\n    public function addDateAttribute($attribute)\n    {\n        if (in_array($attribute, $this->dates)) {\n            return;\n        }\n\n        $this->dates[] = $attribute;\n    }\n\n    /**\n     * addFillable attributes for the model.\n     * @param  array|string|null  $attributes\n     * @return void\n     */\n    public function addFillable($attributes = null)\n    {\n        $this->fillable = array_merge(\n            $this->fillable, is_array($attributes) ? $attributes : func_get_args()\n        );\n    }\n\n    /**\n     * addVisible attributes for the model.\n     * @param  array|string|null  $attributes\n     * @return void\n     */\n    public function addVisible($attributes = null)\n    {\n        $this->visible = array_merge(\n            $this->visible, is_array($attributes) ? $attributes : func_get_args()\n        );\n    }\n}\n"
  },
  {
    "path": "src/Database/Concerns/HasEagerLoadAttachRelation.php",
    "content": "<?php namespace October\\Rain\\Database\\Concerns;\n\nuse Closure;\n\n/**\n * HasEagerLoadAttachRelation eagerly loads all attachments on a model in one pass.\n * Since they share a common type and database table, multiple attachment definitions\n * can be eagerly loaded as a single query.\n */\ntrait HasEagerLoadAttachRelation\n{\n    /**\n     * @var array eagerLoadAttachResultCache\n     */\n    protected $eagerLoadAttachResultCache = [];\n\n    /**\n     * eagerLoadAttachRelation eagerly loads an attachment relationship on a set of models.\n     * @param  string  $relatedModel\n     * @param  array  $models\n     * @param  string  $name\n     * @param  \\Closure  $constraints\n     * @return array|null\n     */\n    protected function eagerLoadAttachRelation(array $models, $name, Closure $constraints)\n    {\n        // Look up relation type\n        $relationType = $this->getModel()->getRelationType($name);\n        if (!$relationType || !in_array($relationType, ['attachOne', 'attachMany'])) {\n            return null;\n        }\n\n        // Only vanilla attachments are supported, pass complex lookups back to Laravel\n        $definition = $this->getModel()->getRelationDefinition($name);\n        if (isset($definition['conditions']) || isset($definition['scope'])) {\n            return null;\n        }\n\n        // Opt-out of the combined eager loading logic\n        if (isset($definition['combineEager']) && $definition['combineEager'] === false) {\n            return null;\n        }\n\n        $relation = $this->getRelation($name);\n        $relatedModel = get_class($relation->getRelated());\n\n        // Perform a global look up attachment without the 'field' constraint\n        // to produce a combined subset of all possible attachment relations.\n        if (!isset($this->eagerLoadAttachResultCache[$relatedModel])) {\n            $relation->addCommonEagerConstraints($models);\n\n            // Note this takes first constraint only. If it becomes a problem one solution\n            // could be to compare the md5 of toSql() to ensure uniqueness. The workaround\n            // for this edge case is to set combineEager => false in the definition.\n            $constraints($relation);\n\n            $this->eagerLoadAttachResultCache[$relatedModel] = $relation->getEager();\n        }\n\n        $results = $this->eagerLoadAttachResultCache[$relatedModel];\n\n        return $relation->match(\n            $relation->initRelation($models, $name),\n            $results->where('field', $name)->values(),\n            $name\n        );\n    }\n}\n"
  },
  {
    "path": "src/Database/Concerns/HasEvents.php",
    "content": "<?php namespace October\\Rain\\Database\\Concerns;\n\n/**\n * HasEvents concern for a model\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasEvents\n{\n    /**\n     * @var array eventsBooted is an array of models booted events\n     */\n    protected static $eventsBooted = [];\n\n    /**\n     * bootNicerEvents to this model, in the format of method overrides.\n     */\n    protected function bootNicerEvents()\n    {\n        if (isset(static::$eventsBooted[static::class])) {\n            return;\n        }\n\n        $nicerEvents = [\n            'creating' => 'beforeCreate',\n            'created' => 'afterCreate',\n            'saving' => 'beforeSave',\n            'saved' => 'afterSave',\n            'updating' => 'beforeUpdate',\n            'updated' => 'afterUpdate',\n            'deleting' => 'beforeDelete',\n            'deleted' => 'afterDelete',\n            'fetching' => 'beforeFetch',\n            'fetched' => 'afterFetch',\n            'replicating' => 'beforeReplicate',\n        ];\n\n        foreach ($nicerEvents as $eventMethod => $method) {\n            self::registerModelEvent($eventMethod, function ($model) use ($method) {\n                $model->fireEvent(\"model.{$method}\");\n                return $model->$method();\n            });\n        }\n\n        // Hooks for late stage attribute changes\n        self::registerModelEvent('creating', function ($model) {\n            $model->fireEvent('model.beforeSaveDone');\n        });\n\n        self::registerModelEvent('updating', function ($model) {\n            $model->fireEvent('model.beforeSaveDone');\n        });\n\n        // Boot event\n        $this->fireEvent('model.afterBoot');\n        $this->afterBoot();\n\n        static::$eventsBooted[static::class] = true;\n    }\n\n    /**\n     * initializeModelEvent is called every time the model is constructed.\n     */\n    protected function initializeModelEvent()\n    {\n        $this->fireEvent('model.afterInit');\n        $this->afterInit();\n    }\n\n    /**\n     * flushEventListeners removes all of the event listeners for the model\n     * Also flush registry of models that had events booted\n     * Allows painless unit testing.\n     * @return void\n     */\n    public static function flushEventListeners()\n    {\n        parent::flushEventListeners();\n        static::$eventsBooted = [];\n    }\n\n    /**\n     * getObservableEvents as their names.\n     * @return array\n     */\n    public function getObservableEvents()\n    {\n        return array_merge(\n            [\n                'creating', 'created', 'updating', 'updated',\n                'deleting', 'deleted', 'saving', 'saved', 'replicating',\n                'trashed', 'restoring', 'restored', 'fetching', 'fetched'\n            ],\n            $this->observables\n        );\n    }\n\n    /**\n     * fetching creates a new native event for handling beforeFetch().\n     * @param \\Closure|string $callback\n     * @return void\n     */\n    public static function fetching($callback)\n    {\n        static::registerModelEvent('fetching', $callback);\n    }\n\n    /**\n     * fetched creates a new native event for handling afterFetch().\n     * @param \\Closure|string $callback\n     * @return void\n     */\n    public static function fetched($callback)\n    {\n        static::registerModelEvent('fetched', $callback);\n    }\n\n    /**\n     * afterBoot is called after the model is constructed for the first time.\n     */\n    protected function afterBoot()\n    {\n        /**\n         * @event model.afterBoot\n         * Called after the model is booted\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.afterBoot', function () use (\\October\\Rain\\Database\\Model $model) {\n         *         \\Log::info(get_class($model) . ' has booted');\n         *     });\n         *\n         */\n    }\n\n    /**\n     * afterInit is called after the model is constructed, a nicer version\n     * of overriding the __construct method.\n     */\n    protected function afterInit()\n    {\n        /**\n         * @event model.afterInit\n         * Called after the model is initialized\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.afterInit', function () use (\\October\\Rain\\Database\\Model $model) {\n         *         \\Log::info(get_class($model) . ' has initialized');\n         *     });\n         *\n         */\n    }\n\n    /**\n     * beforeCreate handles the \"creating\" model event\n     */\n    protected function beforeCreate()\n    {\n        /**\n         * @event model.beforeCreate\n         * Called before the model is created\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.beforeCreate', function () use (\\October\\Rain\\Database\\Model $model) {\n         *         if (!$model->isValid()) {\n         *             throw new \\Exception(\"Invalid Model!\");\n         *         }\n         *     });\n         *\n         */\n    }\n\n    /**\n     * afterCreate handles the \"created\" model event\n     */\n    protected function afterCreate()\n    {\n        /**\n         * @event model.afterCreate\n         * Called after the model is created\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.afterCreate', function () use (\\October\\Rain\\Database\\Model $model) {\n         *         \\Log::info(\"{$model->name} was created!\");\n         *     });\n         *\n         */\n    }\n\n    /**\n     * beforeUpdate handles the \"updating\" model event\n     */\n    protected function beforeUpdate()\n    {\n        /**\n         * @event model.beforeUpdate\n         * Called before the model is updated\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.beforeUpdate', function () use (\\October\\Rain\\Database\\Model $model) {\n         *         if (!$model->isValid()) {\n         *             throw new \\Exception(\"Invalid Model!\");\n         *         }\n         *     });\n         *\n         */\n    }\n\n    /**\n     * afterUpdate handles the \"updated\" model event\n     */\n    protected function afterUpdate()\n    {\n        /**\n         * @event model.afterUpdate\n         * Called after the model is updated\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.afterUpdate', function () use (\\October\\Rain\\Database\\Model $model) {\n         *         if ($model->title !== $model->original['title']) {\n         *             \\Log::info(\"{$model->name} updated its title!\");\n         *         }\n         *     });\n         *\n         */\n    }\n\n    /**\n     * beforeSave handles the \"saving\" model event\n     */\n    protected function beforeSave()\n    {\n        /**\n         * @event model.beforeSave\n         * Called before the model is created or updated\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.beforeSave', function () use (\\October\\Rain\\Database\\Model $model) {\n         *         if (!$model->isValid()) {\n         *             throw new \\Exception(\"Invalid Model!\");\n         *         }\n         *     });\n         *\n         */\n    }\n\n    /**\n     * afterSave handles the \"saved\" model event\n     */\n    protected function afterSave()\n    {\n        /**\n         * @event model.afterSave\n         * Called after the model is created or updated\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.afterSave', function () use (\\October\\Rain\\Database\\Model $model) {\n         *         if ($model->title !== $model->original['title']) {\n         *             \\Log::info(\"{$model->name} updated its title!\");\n         *         }\n         *     });\n         *\n         */\n    }\n\n    /**\n     * beforeDelete handles the \"deleting\" model event\n     */\n    protected function beforeDelete()\n    {\n        /**\n         * @event model.beforeDelete\n         * Called before the model is deleted\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.beforeDelete', function () use (\\October\\Rain\\Database\\Model $model) {\n         *         if (!$model->isAllowedToBeDeleted()) {\n         *             throw new \\Exception(\"You cannot delete me!\");\n         *         }\n         *     });\n         *\n         */\n    }\n\n    /**\n     * afterDelete handles the \"deleted\" model event\n     */\n    protected function afterDelete()\n    {\n        /**\n         * @event model.afterDelete\n         * Called after the model is deleted\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.afterDelete', function () use (\\October\\Rain\\Database\\Model $model) {\n         *         \\Log::info(\"{$model->name} was deleted\");\n         *     });\n         *\n         */\n    }\n\n    /**\n     * beforeFetch handles the \"fetching\" model event\n     */\n    protected function beforeFetch()\n    {\n        /**\n         * @event model.beforeFetch\n         * Called before the model is fetched\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.beforeFetch', function () use (\\October\\Rain\\Database\\Model $model) {\n         *         if (!\\Auth::getUser()->hasAccess('fetch.this.model')) {\n         *             throw new \\Exception(\"You shall not pass!\");\n         *         }\n         *     });\n         *\n         */\n    }\n\n    /**\n     * afterFetch handles the \"fetched\" model event\n     */\n    protected function afterFetch()\n    {\n        /**\n         * @event model.afterFetch\n         * Called after the model is fetched\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.afterFetch', function () use (\\October\\Rain\\Database\\Model $model) {\n         *         \\Log::info(\"{$model->name} was retrieved from the database\");\n         *     });\n         *\n         */\n    }\n\n    /**\n     * beforeReplicate\n     */\n    protected function beforeReplicate()\n    {\n        /**\n         * @event model.beforeReplicate\n         * Called as the model is replicated\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.beforeReplicate', function () use (\\October\\Rain\\Database\\Model $model) {\n         *         \\Log::info(\"{$model->name} is being replicated\");\n         *     });\n         *\n         */\n    }\n\n    /**\n     * beforeRelation is fired on the relation object before it is created\n     */\n    protected function beforeRelation($name, $relation)\n    {\n        /**\n         * @event model.beforeRelation\n         * Called when a new instance of a related model is created\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.beforeRelation', function (string $relationName, \\Illuminate\\Database\\Eloquent\\Relations\\Relation $relatedObject) use (\\October\\Rain\\Database\\Model $model) {\n         *         // Implement scope\n         *         $relatedObject->withLocked();\n         *     });\n         *\n         */\n    }\n\n    /**\n     * beforeRelation is fired on the relation model instance after it is created\n     */\n    protected function afterRelation($name, $model)\n    {\n        /**\n         * @event model.afterRelation\n         * Called when a new instance of a related model is created\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.afterRelation', function (string $relationName, \\October\\Rain\\Database\\Model $relatedModel) use (\\October\\Rain\\Database\\Model $model) {\n         *         // Transfer custom properties\n         *         $relatedModel->isLocked = $model->isLocked;\n         *     });\n         *\n         */\n    }\n}\n"
  },
  {
    "path": "src/Database/Concerns/HasJsonable.php",
    "content": "<?php namespace October\\Rain\\Database\\Concerns;\n\n/**\n * HasJsonable concern for a model\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasJsonable\n{\n    /**\n     * @var array jsonable attribute names that are json encoded and decoded from the database\n     */\n    protected $jsonable = [];\n\n    /**\n     * addJsonable attributes for the model.\n     *\n     * @param  array|string|null  $attributes\n     * @return void\n     */\n    public function addJsonable($attributes = null)\n    {\n        $attributes = is_array($attributes) ? $attributes : func_get_args();\n\n        $this->jsonable = array_merge($this->jsonable, $attributes);\n    }\n\n    /**\n     * isJsonable checks if an attribute is jsonable or not.\n     *\n     * @return array\n     */\n    public function isJsonable($key)\n    {\n        return in_array($key, $this->jsonable);\n    }\n\n    /**\n     * getJsonable attributes name\n     *\n     * @return array\n     */\n    public function getJsonable()\n    {\n        return $this->jsonable;\n    }\n\n    /**\n     * jsonable attributes set for the model.\n     *\n     * @param  array  $jsonable\n     * @return $this\n     */\n    public function jsonable(array $jsonable)\n    {\n        $this->jsonable = $jsonable;\n\n        return $this;\n    }\n\n    /**\n     * addJsonableAttributesToArray\n     * @return array\n     */\n    protected function addJsonableAttributesToArray(array $attributes, array $mutatedAttributes)\n    {\n        foreach ($this->jsonable as $key) {\n            if (\n                !array_key_exists($key, $attributes) ||\n                in_array($key, $mutatedAttributes)\n            ) {\n                continue;\n            }\n\n            // Prevent double decoding of jsonable attributes.\n            if (!is_string($attributes[$key])) {\n                continue;\n            }\n\n            $jsonValue = json_decode($attributes[$key], true);\n            if (json_last_error() === JSON_ERROR_NONE) {\n                $attributes[$key] = $jsonValue;\n            }\n        }\n\n        return $attributes;\n    }\n}\n"
  },
  {
    "path": "src/Database/Concerns/HasNicerPagination.php",
    "content": "<?php namespace October\\Rain\\Database\\Concerns;\n\n/**\n * HasNicerPagination for a query builder\n */\ntrait HasNicerPagination\n{\n    /**\n     * paginateAtPage paginates by passing the page number directly\n     *\n     * @param  int  $perPage\n     * @param  int  $currentPage\n     * @return \\Illuminate\\Contracts\\Pagination\\LengthAwarePaginator\n     */\n    public function paginateAtPage($perPage, $currentPage)\n    {\n        return $this->paginate($perPage, ['*'], 'page', $currentPage);\n    }\n\n    /**\n     * paginateCustom paginates using a custom page name.\n     *\n     * @param  int  $perPage\n     * @param  string $pageName\n     * @return \\Illuminate\\Contracts\\Pagination\\LengthAwarePaginator\n     */\n    public function paginateCustom($perPage, $pageName)\n    {\n        return $this->paginate($perPage, ['*'], $pageName);\n    }\n\n    /**\n     * simplePaginateAtPage simply paginates by passing the page number directly\n     *\n     * @param  int  $perPage\n     * @param  int  $currentPage\n     * @return \\Illuminate\\Contracts\\Pagination\\Paginator\n     */\n    public function simplePaginateAtPage($perPage, $currentPage)\n    {\n        return $this->simplePaginate($perPage, ['*'], 'page', $currentPage);\n    }\n\n    /**\n     * simplePaginateCustom simply paginates using a custom page name.\n     *\n     * @param  int  $perPage\n     * @param  string $pageName\n     * @return \\Illuminate\\Contracts\\Pagination\\Paginator\n     */\n    public function simplePaginateCustom($perPage, $pageName)\n    {\n        return $this->simplePaginate($perPage, ['*'], $pageName);\n    }\n\n    /**\n     * cursorPaginateAtPage paginates using a cursor by passing the cursor directly.\n     *\n     * @param  int  $perPage\n     * @param  \\Illuminate\\Pagination\\Cursor|string|null  $cursor\n     * @return \\Illuminate\\Contracts\\Pagination\\CursorPaginator\n     */\n    public function cursorPaginateAtPage($perPage, $cursor)\n    {\n        return $this->cursorPaginate($perPage, ['*'], 'cursor', $cursor);\n    }\n\n    /**\n     * cursorPaginateCustom paginates using a cursor with a custom cursor name.\n     *\n     * @param  int  $perPage\n     * @param  string $cursorName\n     * @return \\Illuminate\\Contracts\\Pagination\\CursorPaginator\n     */\n    public function cursorPaginateCustom($perPage, $cursorName)\n    {\n        return $this->cursorPaginate($perPage, ['*'], $cursorName);\n    }\n}\n"
  },
  {
    "path": "src/Database/Concerns/HasRelationships.php",
    "content": "<?php namespace October\\Rain\\Database\\Concerns;\n\nuse October\\Rain\\Support\\Arr;\nuse October\\Rain\\Support\\Str;\nuse October\\Rain\\Database\\Relations\\BelongsTo;\nuse October\\Rain\\Database\\Relations\\BelongsToMany;\nuse October\\Rain\\Database\\Relations\\HasMany;\nuse October\\Rain\\Database\\Relations\\HasOne;\nuse October\\Rain\\Database\\Relations\\MorphMany;\nuse October\\Rain\\Database\\Relations\\MorphToMany;\nuse October\\Rain\\Database\\Relations\\MorphTo;\nuse October\\Rain\\Database\\Relations\\MorphOne;\nuse October\\Rain\\Database\\Relations\\AttachMany;\nuse October\\Rain\\Database\\Relations\\AttachOne;\nuse October\\Rain\\Database\\Relations\\HasManyThrough;\nuse October\\Rain\\Database\\Relations\\HasOneThrough;\nuse Illuminate\\Database\\Eloquent\\Model as EloquentModel;\nuse Illuminate\\Database\\Eloquent\\Collection as CollectionBase;\nuse InvalidArgumentException;\n\n/**\n * HasRelationships is a concern used by the \\October\\Rain\\Database\\Model class, employing a\n * cleaner declaration of model relationships.\n *\n * The relation definitions uses an almost identical approach to the relation methods defined\n * by Eloquent, instead using class properties to make the class file less cluttered and keep\n * the logic separated from the definition.\n *\n * Relations should be declared with keys as the relation name and value as a mixed array.\n * The relation type `$morphTo` does not include a class name as the first value.\n *\n * Example:\n *\n *     class Order extends Model\n *     {\n *         protected $hasMany = [\n *             'items' => Item::class\n *         ];\n *     }\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasRelationships\n{\n    /**\n     * @var array hasOne related record, inverse of belongsTo.\n     *\n     *     protected $hasOne = [\n     *         'owner' => [User::class, 'key' => 'user_id']\n     *     ];\n     *\n     */\n    public $hasOne = [];\n\n    /**\n     * @var array hasMany related records, inverse of belongsTo.\n     *\n     *     protected $hasMany = [\n     *         'items' => Item::class\n     *     ];\n     */\n    public $hasMany = [];\n\n    /**\n     * @var array belongsTo another record with a local key attribute\n     *\n     *     protected $belongsTo = [\n     *         'parent' => [Category::class, 'key' => 'parent_id']\n     *     ];\n     */\n    public $belongsTo = [];\n\n    /**\n     * @var array belongsToMany to multiple records using a join table.\n     *\n     *     protected $belongsToMany = [\n     *         'groups' => [Group::class, 'table'=> 'join_groups_users']\n     *     ];\n     */\n    public $belongsToMany = [];\n\n    /**\n     * @var array morphTo another record using local key and type attributes\n     *\n     *     protected $morphTo = [\n     *         'pictures' => []\n     *     ];\n     */\n    public $morphTo = [];\n\n    /**\n     * @var array morphOne related record, inverse of morphTo.\n     *\n     *     protected $morphOne = [\n     *         'log' => [History::class, 'name' => 'user']\n     *     ];\n     */\n    public $morphOne = [];\n\n    /**\n     * @var array morphMany related records, inverse of morphTo.\n     *\n     *     protected $morphMany = [\n     *         'log' => [History::class, 'name' => 'user']\n     *     ];\n     */\n    public $morphMany = [];\n\n    /**\n     * @var array morphToMany to multiple records using a join table.\n     *\n     *     protected $morphToMany = [\n     *         'tag' => [Tag::class, 'table' => 'tagables', 'name' => 'tagable']\n     *     ];\n     */\n    public $morphToMany = [];\n\n    /**\n     * @var array morphedByMany to a polymorphic, inverse many-to-many relationship.\n     *\n     *     public $morphedByMany = [\n     *         'tag' => [Tag::class, 'table' => 'tagables', 'name' => 'tagable']\n     *     ];\n     */\n    public $morphedByMany = [];\n\n    /**\n     * @var array attachOne file attachment.\n     *\n     *     protected $attachOne = [\n     *         'picture' => [\\October\\Rain\\Database\\Attach\\File::class, 'public' => false]\n     *     ];\n     */\n    public $attachOne = [];\n\n    /**\n     * @var array attachMany file attachments.\n     *\n     *     protected $attachMany = [\n     *         'pictures' => [\\October\\Rain\\Database\\Attach\\File::class, 'name'=> 'imageable']\n     *     ];\n     */\n    public $attachMany = [];\n\n    /**\n     * @var array hasManyThrough is related records through another record.\n     *\n     *     protected $hasManyThrough = [\n     *         'posts' => [Post::class, 'through' => User::class]\n     *     ];\n     */\n    public $hasManyThrough = [];\n\n    /**\n     * @var array hasOneThrough is a related record through another record.\n     *\n     *     protected $hasOneThrough = [\n     *         'post' => [Post::class, 'through' => User::class]\n     *     ];\n     */\n    public $hasOneThrough = [];\n\n    /**\n     * @var array relationTypes expected, used to cycle and verify relationships.\n     */\n    protected static $relationTypes = [\n        'hasOne',\n        'hasMany',\n        'belongsTo',\n        'belongsToMany',\n        'morphTo',\n        'morphOne',\n        'morphMany',\n        'morphToMany',\n        'morphedByMany',\n        'attachOne',\n        'attachMany',\n        'hasOneThrough',\n        'hasManyThrough'\n    ];\n\n    //\n    // Relations\n    //\n\n    /**\n     * hasRelation checks if model has a relationship by supplied name\n     */\n    public function hasRelation(string $name): bool\n    {\n        return $this->getRelationType($name) !== null;\n    }\n\n    /**\n     * getRelationDefinition returns relationship details from a supplied name\n     */\n    public function getRelationDefinition(string $name): array\n    {\n        if (($type = $this->getRelationType($name)) !== null) {\n            return (array) $this->{$type}[$name] + $this->getRelationDefaults($type);\n        }\n\n        return [];\n    }\n\n    /**\n     * getRelationDefinitions returns relationship details for all relations\n     * defined on this model\n     * @return array\n     */\n    public function getRelationDefinitions()\n    {\n        $result = [];\n\n        foreach (static::$relationTypes as $type) {\n            $result[$type] = $this->{$type};\n\n            // Apply default values for the relation type\n            if ($defaults = $this->getRelationDefaults($type)) {\n                foreach ($result[$type] as $relation => $options) {\n                    $result[$type][$relation] = (array) $options + $defaults;\n                }\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * getRelationType returns a relationship type based on a supplied name\n     * @param string $name Relation name\n     * @return \\October\\Rain\\Database\\Relation\n     */\n    public function getRelationType($name)\n    {\n        foreach (static::$relationTypes as $type) {\n            if (isset($this->{$type}[$name])) {\n                return $type;\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * isRelationTypeSingular returns true if the relation is expected to return\n     * a single record versus a collection of records.\n     */\n    public function isRelationTypeSingular($name): bool\n    {\n        return in_array($this->getRelationType($name), [\n            'hasOne',\n            'belongsTo',\n            'morphTo',\n            'morphOne',\n            'attachOne',\n            'hasOneThrough'\n        ]);\n    }\n\n    /**\n     * makeRelation returns a relation class object, supporting nested relations with\n     * dot notation\n     * @param string $name\n     * @return \\Model|null\n     */\n    public function makeRelation($name)\n    {\n        if (str_contains($name, '.')) {\n            $model = $this;\n            $parts = explode('.', $name);\n            while ($relationName = array_shift($parts)) {\n                if (!$model = $model->makeRelation($relationName)) {\n                    return null;\n                }\n            }\n            return $model;\n        }\n\n        $relation = $this->getRelationDefinition($name);\n        $relationType = $this->getRelationType($name);\n\n        if ($relationType === 'morphTo' || !isset($relation[0])) {\n            return null;\n        }\n\n        return $this->makeRelationInternal($name, $relation[0]);\n    }\n\n    /**\n     * makeRelationInternal is used internally to create a new related instance. It also\n     * fires the `afterRelation` to extend the created instance.\n     */\n    protected function makeRelationInternal(string $relationName, string $relationClass)\n    {\n        $model = $this->newRelatedInstance($relationClass);\n\n        $this->fireEvent('model.afterRelation', [$relationName, $model]);\n        $this->afterRelation($relationName, $model);\n\n        return $model;\n    }\n\n    /**\n     * isRelationPushable determines whether the specified relation should be saved\n     * when `push()` is called instead of `save()` on the model. Defaults to `true`.\n     */\n    public function isRelationPushable(string $name): bool\n    {\n        $definition = $this->getRelationDefinition($name);\n\n        if (!array_key_exists('push', $definition)) {\n            // @deprecated v4 this should default to false\n            return true;\n        }\n\n        return (bool) $definition['push'];\n    }\n\n    /**\n     * getRelationDefaults returns default relation arguments for a given type.\n     * @param string $type\n     * @return array\n     */\n    protected function getRelationDefaults($type)\n    {\n        switch ($type) {\n            case 'attachOne':\n            case 'attachMany':\n                return ['order' => 'sort_order', 'delete' => true];\n\n            default:\n                return [];\n        }\n    }\n\n    /**\n     * handleRelation looks for the relation and does the correct magic as Eloquent would require\n     * inside relation methods. For more information, read the documentation of the mentioned property.\n     * @param string $relationName the relation key, camel-case version\n     * @return \\Illuminate\\Database\\Eloquent\\Relations\\Relation\n     */\n    protected function handleRelation($relationName)\n    {\n        $relationType = $this->getRelationType($relationName);\n        $relation = $this->getRelationDefinition($relationName);\n\n        if (!isset($relation[0]) && $relationType !== 'morphTo') {\n            throw new InvalidArgumentException(sprintf(\n                \"Relation '%s' on model '%s' should have at least a classname.\",\n                $relationName,\n                static::class\n            ));\n        }\n\n        if (isset($relation[0]) && $relationType === 'morphTo') {\n            throw new InvalidArgumentException(sprintf(\n                \"Relation '%s' on model '%s' is a morphTo relation and should not contain additional arguments.\",\n                $relationName,\n                static::class\n            ));\n        }\n\n        switch ($relationType) {\n            case 'hasOne':\n            case 'hasMany':\n                $relation = $this->validateRelationArgs($relationName, ['key', 'otherKey']);\n                $relationObj = $this->$relationType($relation[0], $relation['key'], $relation['otherKey'], $relationName);\n                break;\n\n            case 'belongsTo':\n                $relation = $this->validateRelationArgs($relationName, ['key', 'otherKey']);\n                $relationObj = $this->$relationType($relation[0], $relation['key'], $relation['otherKey'], $relationName);\n                break;\n\n            case 'belongsToMany':\n                $relation = $this->validateRelationArgs($relationName, ['table', 'key', 'otherKey', 'parentKey', 'relatedKey', 'pivot', 'timestamps']);\n                $relationObj = $this->$relationType($relation[0], $relation['table'], $relation['key'], $relation['otherKey'], $relation['parentKey'], $relation['relatedKey'], $relationName);\n                break;\n\n            case 'morphTo':\n                $relation = $this->validateRelationArgs($relationName, ['name', 'type', 'id']);\n                $relationObj = $this->$relationType($relation['name'] ?: $relationName, $relation['type'], $relation['id']);\n                break;\n\n            case 'morphOne':\n            case 'morphMany':\n                $relation = $this->validateRelationArgs($relationName, ['type', 'id', 'key'], ['name']);\n                $relationObj = $this->$relationType($relation[0], $relation['name'], $relation['type'], $relation['id'], $relation['key'], $relationName);\n                break;\n\n            case 'morphToMany':\n                $relation = $this->validateRelationArgs($relationName, ['table', 'key', 'otherKey', 'parentKey', 'relatedKey', 'pivot', 'timestamps'], ['name']);\n                $relationObj = $this->$relationType($relation[0], $relation['name'], $relation['table'], $relation['key'], $relation['otherKey'], $relation['parentKey'], $relation['relatedKey'], false, $relationName);\n                break;\n\n            case 'morphedByMany':\n                $relation = $this->validateRelationArgs($relationName, ['table', 'key', 'otherKey', 'parentKey', 'relatedKey', 'pivot', 'timestamps'], ['name']);\n                $relationObj = $this->$relationType($relation[0], $relation['name'], $relation['table'], $relation['key'], $relation['otherKey'], $relation['parentKey'], $relation['relatedKey'], $relationName);\n                break;\n\n            case 'attachOne':\n            case 'attachMany':\n                $relation = $this->validateRelationArgs($relationName, ['public', 'key']);\n                $relationObj = $this->$relationType($relation[0], $relation['public'], $relation['key'], $relationName);\n                break;\n\n            case 'hasOneThrough':\n            case 'hasManyThrough':\n                $relation = $this->validateRelationArgs($relationName, ['key', 'throughKey', 'otherKey', 'secondOtherKey'], ['through']);\n                $relationObj = $this->$relationType($relation[0], $relation['through'], $relation['key'], $relation['throughKey'], $relation['otherKey'], $relation['secondOtherKey'], $relationName);\n                break;\n\n            default:\n                throw new InvalidArgumentException(sprintf(\"There is no such relation type known as '%s' on model '%s'.\", $relationType, static::class));\n        }\n\n        // Relation hook event\n        $this->fireEvent('model.beforeRelation', [$relationName, $relationObj]);\n        $this->beforeRelation($relationName, $relationObj);\n\n        return $relationObj;\n    }\n\n    /**\n     * validateRelationArgs supplied relation arguments\n     */\n    protected function validateRelationArgs($relationName, $optional, $required = [])\n    {\n        $relation = $this->getRelationDefinition($relationName);\n\n        // Query filter arguments\n        $filters = ['scope', 'conditions', 'order', 'pivot', 'timestamps', 'push', 'count', 'default'];\n\n        foreach (array_merge($optional, $filters) as $key) {\n            if (!array_key_exists($key, $relation)) {\n                $relation[$key] = null;\n            }\n        }\n\n        $missingRequired = [];\n        foreach ($required as $key) {\n            if (!array_key_exists($key, $relation)) {\n                $missingRequired[] = $key;\n            }\n        }\n\n        if ($missingRequired) {\n            throw new InvalidArgumentException(sprintf(\n                'Relation \"%s\" on model \"%s\" should contain the following key(s): %s',\n                $relationName,\n                static::class,\n                implode(', ', $missingRequired)\n            ));\n        }\n\n        return $relation;\n    }\n\n    /**\n     * getRelationCustomClass returns a custom relation class name for\n     * the relation or null if none is found.\n     */\n    protected function getRelationCustomClass(string $name): ?string\n    {\n        if (($type = $this->getRelationType($name)) !== null) {\n            return $this->{$type}[$name]['relationClass'] ?? null;\n        }\n\n        return null;\n    }\n\n    /**\n     * hasOne defines a one-to-one relationship.\n     * This code is a duplicate of Eloquent but uses a Rain relation class.\n     * @return \\October\\Rain\\Database\\Relations\\HasOne\n     */\n    public function hasOne($related, $primaryKey = null, $localKey = null, $relationName = null)\n    {\n        if (is_null($relationName)) {\n            $relationName = $this->getRelationCaller();\n        }\n\n        $instance = $this->makeRelationInternal($relationName, $related);\n\n        $primaryKey = $primaryKey ?: $this->getForeignKey();\n\n        $localKey = $localKey ?: $this->getKeyName();\n\n        $relationClass = $this->getRelationCustomClass($relationName) ?: HasOne::class;\n\n        return new $relationClass($instance->newQuery(), $this, $instance->getTable().'.'.$primaryKey, $localKey, $relationName);\n    }\n\n    /**\n     * morphOne defines a polymorphic one-to-one relationship.\n     * This code is a duplicate of Eloquent but uses a Rain relation class.\n     * @return \\October\\Rain\\Database\\Relations\\MorphOne\n     */\n    public function morphOne($related, $name, $type = null, $id = null, $localKey = null, $relationName = null)\n    {\n        if (is_null($relationName)) {\n            $relationName = $this->getRelationCaller();\n        }\n\n        $instance = $this->makeRelationInternal($relationName, $related);\n\n        [$type, $id] = $this->getMorphs($name, $type, $id);\n\n        $table = $instance->getTable();\n\n        $localKey = $localKey ?: $this->getKeyName();\n\n        $relationClass = $this->getRelationCustomClass($relationName) ?: MorphOne::class;\n\n        return new $relationClass($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey, $relationName);\n    }\n\n    /**\n     * belongsTo defines an inverse one-to-one or many relationship. Overridden from\n     * \\Eloquent\\Model to allow the usage of the intermediary methods to handle the\n     * relationsData array.\n     * @return \\October\\Rain\\Database\\Relations\\BelongsTo\n     */\n    public function belongsTo($related, $foreignKey = null, $parentKey = null, $relationName = null)\n    {\n        if (is_null($relationName)) {\n            $relationName = $this->getRelationCaller();\n        }\n\n        $instance = $this->makeRelationInternal($relationName, $related);\n\n        if (is_null($foreignKey)) {\n            $foreignKey = snake_case($relationName).'_id';\n        }\n\n        $parentKey = $parentKey ?: $instance->getKeyName();\n\n        $relationClass = $this->getRelationCustomClass($relationName) ?: BelongsTo::class;\n\n        return new $relationClass($instance->newQuery(), $this, $foreignKey, $parentKey, $relationName);\n    }\n\n    /**\n     * morphTo defines a polymorphic, inverse one-to-one or many relationship.\n     * Overridden from \\Eloquent\\Model to allow the usage of the intermediary\n     * methods to handle the relation.\n     * @return \\October\\Rain\\Database\\Relations\\BelongsTo\n     */\n    public function morphTo($name = null, $type = null, $id = null, $ownerKey = null)\n    {\n        if (is_null($name)) {\n            $name = $this->getRelationCaller();\n        }\n\n        [$type, $id] = $this->getMorphs(Str::snake($name), $type, $id);\n\n        return empty($class = $this->{$type})\n                    ? $this->morphEagerTo($name, $type, $id, $ownerKey)\n                    : $this->morphInstanceTo($class, $name, $type, $id, $ownerKey);\n    }\n\n    /**\n     * morphEagerTo defines a polymorphic, inverse one-to-one or many relationship.\n     * @param  string  $name\n     * @param  string  $type\n     * @param  string  $id\n     * @param  string  $ownerKey\n     * @return \\Illuminate\\Database\\Eloquent\\Relations\\MorphTo\n     */\n    protected function morphEagerTo($name, $type, $id, $ownerKey)\n    {\n        return new MorphTo(\n            $this->newQuery()->setEagerLoads([]),\n            $this,\n            $id,\n            $ownerKey,\n            $type,\n            $name\n        );\n    }\n\n    /**\n     * morphInstanceTo defines a polymorphic, inverse one-to-one or many relationship\n     * @param  string  $target\n     * @param  string  $name\n     * @param  string  $type\n     * @param  string  $id\n     * @param  string  $ownerKey\n     * @return \\Illuminate\\Database\\Eloquent\\Relations\\MorphTo\n     */\n    protected function morphInstanceTo($target, $name, $type, $id, $ownerKey)\n    {\n        $instance = $this->newRelatedInstance(\n            static::getActualClassNameForMorph($target)\n        );\n\n        return new MorphTo(\n            $instance->newQuery(),\n            $this,\n            $id,\n            $ownerKey ?? $instance->getKeyName(),\n            $type,\n            $name\n        );\n    }\n\n    /**\n     * hasMany defines a one-to-many relationship.\n     * This code is a duplicate of Eloquent but uses a Rain relation class.\n     * @return \\October\\Rain\\Database\\Relations\\HasMany\n     */\n    public function hasMany($related, $primaryKey = null, $localKey = null, $relationName = null)\n    {\n        if (is_null($relationName)) {\n            $relationName = $this->getRelationCaller();\n        }\n\n        $instance = $this->makeRelationInternal($relationName, $related);\n\n        $primaryKey = $primaryKey ?: $this->getForeignKey();\n\n        $localKey = $localKey ?: $this->getKeyName();\n\n        $relationClass = $this->getRelationCustomClass($relationName) ?: HasMany::class;\n\n        return new $relationClass($instance->newQuery(), $this, $instance->getTable().'.'.$primaryKey, $localKey, $relationName);\n    }\n\n    /**\n     * hasManyThrough defines a has-many-through relationship.\n     * This code is a duplicate of Eloquent but uses a Rain relation class.\n     * @return \\October\\Rain\\Database\\Relations\\HasManyThrough\n     */\n    public function hasManyThrough($related, $through, $primaryKey = null, $throughKey = null, $localKey = null, $secondLocalKey = null, $relationName = null)\n    {\n        if (is_null($relationName)) {\n            $relationName = $this->getRelationCaller();\n        }\n\n        $throughInstance = new $through;\n\n        $primaryKey = $primaryKey ?: $this->getForeignKey();\n\n        $throughKey = $throughKey ?: $throughInstance->getForeignKey();\n\n        $localKey = $localKey ?: $this->getKeyName();\n\n        $secondLocalKey = $secondLocalKey ?: $throughInstance->getKeyName();\n\n        $instance = $this->makeRelationInternal($relationName, $related);\n\n        $relationClass = $this->getRelationCustomClass($relationName) ?: HasManyThrough::class;\n\n        return new $relationClass($instance->newQuery(), $this, $throughInstance, $primaryKey, $throughKey, $localKey, $secondLocalKey, $relationName);\n    }\n\n    /**\n     * hasOneThrough define a has-one-through relationship.\n     * This code is a duplicate of Eloquent but uses a Rain relation class.\n     * @return \\October\\Rain\\Database\\Relations\\HasOneThrough\n     */\n    public function hasOneThrough($related, $through, $primaryKey = null, $throughKey = null, $localKey = null, $secondLocalKey = null, $relationName = null)\n    {\n        if (is_null($relationName)) {\n            $relationName = $this->getRelationCaller();\n        }\n\n        $throughInstance = new $through;\n\n        $primaryKey = $primaryKey ?: $this->getForeignKey();\n\n        $throughKey = $throughKey ?: $throughInstance->getForeignKey();\n\n        $localKey = $localKey ?: $this->getKeyName();\n\n        $secondLocalKey = $secondLocalKey ?: $throughInstance->getKeyName();\n\n        $instance = $this->makeRelationInternal($relationName, $related);\n\n        $relationClass = $this->getRelationCustomClass($relationName) ?: HasOneThrough::class;\n\n        return new $relationClass($instance->newQuery(), $this, $throughInstance, $primaryKey, $throughKey, $localKey, $secondLocalKey, $relationName);\n    }\n\n    /**\n     * morphMany defines a polymorphic one-to-many relationship.\n     * This code is a duplicate of Eloquent but uses a Rain relation class.\n     * @return \\October\\Rain\\Database\\Relations\\MorphMany\n     */\n    public function morphMany($related, $name, $type = null, $id = null, $localKey = null, $relationName = null)\n    {\n        if (is_null($relationName)) {\n            $relationName = $this->getRelationCaller();\n        }\n\n        $instance = $this->makeRelationInternal($relationName, $related);\n\n        [$type, $id] = $this->getMorphs($name, $type, $id);\n\n        $table = $instance->getTable();\n\n        $localKey = $localKey ?: $this->getKeyName();\n\n        $relationClass = $this->getRelationCustomClass($relationName) ?: MorphMany::class;\n\n        return new $relationClass($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $localKey, $relationName);\n    }\n\n    /**\n     * belongsToMany defines a many-to-many relationship.\n     * This code is almost a duplicate of Eloquent but uses a Rain relation class.\n     * @return \\October\\Rain\\Database\\Relations\\BelongsToMany\n     */\n    public function belongsToMany($related, $table = null, $primaryKey = null, $foreignKey = null, $parentKey = null, $relatedKey = null, $relationName = null)\n    {\n        if (is_null($relationName)) {\n            $relationName = $this->getRelationCaller();\n        }\n\n        $instance = $this->makeRelationInternal($relationName, $related);\n\n        $primaryKey = $primaryKey ?: $this->getForeignKey();\n\n        $foreignKey = $foreignKey ?: $instance->getForeignKey();\n\n        if (is_null($table)) {\n            $table = $this->joiningTable($related);\n        }\n\n        $relationClass = $this->getRelationCustomClass($relationName) ?: BelongsToMany::class;\n\n        return new $relationClass(\n            $instance->newQuery(),\n            $this,\n            $table,\n            $primaryKey,\n            $foreignKey,\n            $parentKey ?: $this->getKeyName(),\n            $relatedKey ?: $instance->getKeyName(),\n            $relationName\n        );\n    }\n\n    /**\n     * morphToMany defines a polymorphic many-to-many relationship.\n     * This code is almost a duplicate of Eloquent but uses a Rain relation class.\n     * @return \\October\\Rain\\Database\\Relations\\MorphToMany\n     */\n    public function morphToMany($related, $name, $table = null, $primaryKey = null, $foreignKey = null, $parentKey = null, $relatedKey = null, $inverse = false, $relationName = null)\n    {\n        if (is_null($relationName)) {\n            $relationName = $this->getRelationCaller();\n        }\n\n        $instance = $this->makeRelationInternal($relationName, $related);\n\n        $primaryKey = $primaryKey ?: $name.'_id';\n\n        $foreignKey = $foreignKey ?: $instance->getForeignKey();\n\n        $table = $table ?: Str::plural($name);\n\n        $relationClass = $this->getRelationCustomClass($relationName) ?: MorphToMany::class;\n\n        return new $relationClass(\n            $instance->newQuery(),\n            $this,\n            $name,\n            $table,\n            $primaryKey,\n            $foreignKey,\n            $parentKey ?: $this->getKeyName(),\n            $relatedKey ?: $instance->getKeyName(),\n            $relationName,\n            $inverse\n        );\n    }\n\n    /**\n     * morphedByMany defines a polymorphic many-to-many inverse relationship.\n     * This code is almost a duplicate of Eloquent but uses a Rain relation class.\n     * @return \\October\\Rain\\Database\\Relations\\MorphToMany\n     */\n    public function morphedByMany($related, $name, $table = null, $primaryKey = null, $foreignKey = null, $parentKey = null, $relatedKey = null, $relationName = null)\n    {\n        if (is_null($relationName)) {\n            $relationName = $this->getRelationCaller();\n        }\n\n        $primaryKey = $primaryKey ?: $this->getForeignKey();\n\n        $foreignKey = $foreignKey ?: $name.'_id';\n\n        return $this->morphToMany(\n            $related,\n            $name,\n            $table,\n            $primaryKey,\n            $foreignKey,\n            $parentKey,\n            $relatedKey,\n            true,\n            $relationName\n        );\n    }\n\n    /**\n     * attachOne defines an attachment one-to-one relationship.\n     * This code is a duplicate of Eloquent but uses a Rain relation class.\n     * @return \\October\\Rain\\Database\\Relations\\MorphOne\n     */\n    public function attachOne($related, $isPublic = true, $localKey = null, $relationName = null)\n    {\n        if (is_null($relationName)) {\n            $relationName = $this->getRelationCaller();\n        }\n\n        $instance = $this->makeRelationInternal($relationName, $related);\n\n        [$type, $id] = $this->getMorphs('attachment', null, null);\n\n        $table = $instance->getTable();\n\n        $localKey = $localKey ?: $this->getKeyName();\n\n        $relationClass = $this->getRelationCustomClass($relationName) ?: AttachOne::class;\n\n        return new $relationClass($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $isPublic, $localKey, $relationName);\n    }\n\n    /**\n     * attachMany defines an attachment one-to-many relationship.\n     * This code is a duplicate of Eloquent but uses a Rain relation class.\n     * @return \\October\\Rain\\Database\\Relations\\MorphMany\n     */\n    public function attachMany($related, $isPublic = null, $localKey = null, $relationName = null)\n    {\n        if (is_null($relationName)) {\n            $relationName = $this->getRelationCaller();\n        }\n\n        $instance = $this->makeRelationInternal($relationName, $related);\n\n        [$type, $id] = $this->getMorphs('attachment', null, null);\n\n        $table = $instance->getTable();\n\n        $localKey = $localKey ?: $this->getKeyName();\n\n        $relationClass = $this->getRelationCustomClass($relationName) ?: AttachMany::class;\n\n        return new $relationClass($instance->newQuery(), $this, $table.'.'.$type, $table.'.'.$id, $isPublic, $localKey, $relationName);\n    }\n\n    /**\n     * getRelationCaller finds the calling function name from the stack trace.\n     */\n    protected function getRelationCaller()\n    {\n        $backtrace = debug_backtrace(false);\n\n        $caller = $backtrace[2]['function'] === 'handleRelation'\n            ? $backtrace[4]\n            : $backtrace[2];\n\n        return $caller['function'];\n    }\n\n    /**\n     * getRelationSimpleValue returns a relation key value(s), not as an object.\n     */\n    public function getRelationSimpleValue($relationName)\n    {\n        return $this->$relationName()->getSimpleValue();\n    }\n\n    /**\n     * setRelationSimpleValue sets a relation value directly from its attribute.\n     */\n    protected function setRelationSimpleValue($relationName, $value)\n    {\n        $this->$relationName()->setSimpleValue($value);\n    }\n\n    /**\n     * performDeleteOnRelations locates relations with delete flag and cascades the\n     * delete event. This is called before the parent model is deleted. This method\n     * checks in with the Multisite trait to preserve shared relations.\n     *\n     * @see \\October\\Rain\\Database\\Traits\\Multisite::canDeleteMultisiteRelation\n     */\n    protected function performDeleteOnRelations()\n    {\n        $definitions = $this->getRelationDefinitions();\n        $useMultisite = $this->isClassInstanceOf(\\October\\Contracts\\Database\\MultisiteInterface::class) && $this->isMultisiteEnabled();\n\n        foreach ($definitions as $type => $relations) {\n            foreach ($relations as $name => $options) {\n                // Detect and preserve shared multisite relationships\n                if ($useMultisite && !$this->canDeleteMultisiteRelation($name, $type)) {\n                    continue;\n                }\n\n                // Belongs-To-Many should clean up after itself by default\n                 if (in_array($type, ['belongsToMany', 'morphToMany', 'morphedByMany'])) {\n                    if (!Arr::get($options, 'detach', true)) {\n                        continue;\n                    }\n\n                    $this->{$name}()->detach();\n                }\n                // Hard 'delete' definition\n                else {\n                    if (!Arr::get($options, 'delete', false)) {\n                        // Attachment relations should be orphaned when delete is false\n                        if (in_array($type, ['attachOne', 'attachMany'])) {\n                            $this->{$name}()->update([\n                                'attachment_id' => null,\n                                'attachment_type' => null,\n                                'field' => null,\n                            ]);\n                        }\n\n                        continue;\n                    }\n\n                    if (!$relation = $this->{$name}) {\n                        continue;\n                    }\n\n                    if ($relation instanceof EloquentModel) {\n                        $relation->forceDelete();\n                    }\n                    elseif ($relation instanceof CollectionBase) {\n                        $relation->each(function ($model) {\n                            $model->forceDelete();\n                        });\n                    }\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Database/Concerns/HasReplication.php",
    "content": "<?php namespace October\\Rain\\Database\\Concerns;\n\nuse App;\n\n/**\n * HasReplication for a model\n *\n * @see October\\Rain\\Database\\Replicator\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasReplication\n{\n    /**\n     * replicateWithRelations replicates the model into a new, non-existing instance,\n     * including replicating relations.\n     *\n     * @param  array|null  $except\n     * @return static\n     */\n    public function replicateWithRelations(?array $except = null)\n    {\n        return App::makeWith('db.replicator', ['model' => $this])->replicate($except);\n    }\n\n    /**\n     * duplicateWithRelations replicates a model with special multisite duplication logic.\n     * To avoid duplication of has many relations, the logic only propagates relations on\n     * the parent model since they are shared via site_root_id beyond this point.\n     *\n     * @param  array|null  $except\n     * @return static\n     */\n    public function duplicateWithRelations(?array $except = null)\n    {\n        return App::makeWith('db.replicator', ['model' => $this])->duplicate($except);\n    }\n\n    /**\n     * newReplicationInstance returns a new instance used by the replicator\n     */\n    public function newReplicationInstance($attributes)\n    {\n        $instance = $this->newInstance();\n\n        $instance->setRawAttributes($attributes);\n\n        $instance->fireModelEvent('replicating', false);\n\n        return $instance;\n    }\n\n    /**\n     * isRelationReplicable determines whether the specified relation should be replicated\n     */\n    public function isRelationReplicable(string $name): bool\n    {\n        $relationType = $this->getRelationType($name);\n\n        // Skip read-only relations\n        if (in_array($relationType, ['morphTo', 'hasManyThrough', 'hasOneThrough'])) {\n            return false;\n        }\n\n        $definition = $this->getRelationDefinition($name);\n        if (!array_key_exists('replicate', $definition)) {\n            return true;\n        }\n\n        return (bool) $definition['replicate'];\n    }\n}\n"
  },
  {
    "path": "src/Database/Connections/Connection.php",
    "content": "<?php namespace October\\Rain\\Database\\Connections;\n\nuse October\\Rain\\Database\\QueryBuilder;\nuse Illuminate\\Database\\Connection as ConnectionBase;\n\n/**\n * @deprecated see \\October\\Rain\\Database\\Connections\\ExtendsConnection\n */\nclass Connection extends ConnectionBase\n{\n    /**\n     * query builder instance\n     * @return \\October\\Rain\\Database\\QueryBuilder\n     */\n    public function query()\n    {\n        return new QueryBuilder(\n            $this,\n            $this->getQueryGrammar(),\n            $this->getPostProcessor()\n        );\n    }\n\n    /**\n     * logQuery in the connection's query log\n     * @param  string  $query\n     * @param  array   $bindings\n     * @param  float|null  $time\n     * @return void\n     */\n    public function logQuery($query, $bindings, $time = null)\n    {\n        if (isset($this->events)) {\n            $this->events->dispatch('illuminate.query', [$query, $bindings, $time, $this->getName()]);\n        }\n\n        parent::logQuery($query, $bindings, $time);\n    }\n\n    /**\n     * fireConnectionEvent for this connection\n     * @param  string  $event\n     * @return void\n     */\n    protected function fireConnectionEvent($event)\n    {\n        if (isset($this->events)) {\n            $this->events->dispatch('connection.'.$this->getName().'.'.$event, $this);\n        }\n\n        parent::fireConnectionEvent($event);\n    }\n}\n"
  },
  {
    "path": "src/Database/Connections/ExtendsConnection.php",
    "content": "<?php namespace October\\Rain\\Database\\Connections;\n\nuse October\\Rain\\Database\\QueryBuilder;\n\n/**\n * ExtendsConnection replaces the query builder in the connection,\n * and modifies logging events. This trait must extend a connection\n * class that extends the `Illuminate\\Database\\Connection` class.\n */\ntrait ExtendsConnection\n{\n    /**\n     * query builder instance\n     * @return \\October\\Rain\\Database\\QueryBuilder\n     */\n    public function query()\n    {\n        return new QueryBuilder(\n            $this,\n            $this->getQueryGrammar(),\n            $this->getPostProcessor()\n        );\n    }\n\n    /**\n     * logQuery in the connection's query log\n     * @param  string  $query\n     * @param  array   $bindings\n     * @param  float|null  $time\n     * @return void\n     */\n    public function logQuery($query, $bindings, $time = null)\n    {\n        if (isset($this->events)) {\n            $this->events->dispatch('illuminate.query', [$query, $bindings, $time, $this->getName()]);\n        }\n\n        parent::logQuery($query, $bindings, $time);\n    }\n\n    /**\n     * fireConnectionEvent for this connection\n     * @param  string  $event\n     * @return void\n     */\n    protected function fireConnectionEvent($event)\n    {\n        if (isset($this->events)) {\n            $this->events->dispatch('connection.'.$this->getName().'.'.$event, $this);\n        }\n\n        parent::fireConnectionEvent($event);\n    }\n}"
  },
  {
    "path": "src/Database/Connections/MariaDbConnection.php",
    "content": "<?php namespace October\\Rain\\Database\\Connections;\n\nuse Illuminate\\Database\\MariaDbConnection as MariaDbConnectionBase;\n\n/**\n * MariaDbConnection implements connection extension\n */\nclass MariaDbConnection extends MariaDbConnectionBase\n{\n    use \\October\\Rain\\Database\\Connections\\ExtendsConnection;\n}\n"
  },
  {
    "path": "src/Database/Connections/MySqlConnection.php",
    "content": "<?php namespace October\\Rain\\Database\\Connections;\n\nuse Illuminate\\Database\\MySqlConnection as MySqlConnectionBase;\n\n/**\n * MySqlConnection implements connection extension\n */\nclass MySqlConnection extends MySqlConnectionBase\n{\n    use \\October\\Rain\\Database\\Connections\\ExtendsConnection;\n}\n"
  },
  {
    "path": "src/Database/Connections/PostgresConnection.php",
    "content": "<?php namespace October\\Rain\\Database\\Connections;\n\nuse Illuminate\\Database\\PostgresConnection as PostgresConnectionBase;\n\n/**\n * PostgresConnection implements connection extension\n */\nclass PostgresConnection extends PostgresConnectionBase\n{\n    use \\October\\Rain\\Database\\Connections\\ExtendsConnection;\n}\n"
  },
  {
    "path": "src/Database/Connections/SQLiteConnection.php",
    "content": "<?php namespace October\\Rain\\Database\\Connections;\n\nuse Illuminate\\Database\\SQLiteConnection as SQLiteConnectionBase;\n\n/**\n * SQLiteConnection implements connection extension\n */\nclass SQLiteConnection extends SQLiteConnectionBase\n{\n    use \\October\\Rain\\Database\\Connections\\ExtendsConnection;\n}\n"
  },
  {
    "path": "src/Database/Connections/SqlServerConnection.php",
    "content": "<?php namespace October\\Rain\\Database\\Connections;\n\nuse Illuminate\\Database\\SqlServerConnection as SqlServerConnectionBase;\n\n/**\n * SqlServerConnectionBase implements connection extension\n */\nclass SqlServerConnection extends SqlServerConnectionBase\n{\n    use \\October\\Rain\\Database\\Connections\\ExtendsConnection;\n}\n"
  },
  {
    "path": "src/Database/Connectors/ConnectionFactory.php",
    "content": "<?php namespace October\\Rain\\Database\\Connectors;\n\nuse Illuminate\\Support\\Arr;\nuse Illuminate\\Database\\Connectors\\ConnectionFactory as ConnectionFactoryBase;\nuse October\\Rain\\Database\\Connections\\Connection;\nuse October\\Rain\\Database\\Connections\\MariaDbConnection;\nuse October\\Rain\\Database\\Connections\\MySqlConnection;\nuse October\\Rain\\Database\\Connections\\SQLiteConnection;\nuse October\\Rain\\Database\\Connections\\PostgresConnection;\nuse October\\Rain\\Database\\Connections\\SqlServerConnection;\nuse InvalidArgumentException;\nuse PDOException;\n\nclass ConnectionFactory extends ConnectionFactoryBase\n{\n    /**\n     * Carbon copy of parent. Except Laravel creates an \"uncatchable\" exception,\n     * this is resolved as part of the override below.\n     *\n     * @param  array  $config\n     * @return \\Closure\n     */\n    protected function createPdoResolverWithHosts(array $config)\n    {\n        return function () use ($config) {\n            foreach (Arr::shuffle($hosts = $this->parseHosts($config)) as $key => $host) {\n                $config['host'] = $host;\n\n                try {\n                    return $this->createConnector($config)->connect($config);\n                }\n                catch (PDOException $e) {\n                }\n            }\n\n            throw $e;\n        };\n    }\n\n    /**\n     * Create a new connection instance.\n     *\n     * @param  string   $driver\n     * @param  \\PDO     $connection\n     * @param  string   $database\n     * @param  string   $prefix\n     * @param  array    $config\n     * @return \\Illuminate\\Database\\Connection\n     *\n     * @throws \\InvalidArgumentException\n     */\n    protected function createConnection($driver, $connection, $database, $prefix = '', array $config = [])\n    {\n        if ($resolver = Connection::getResolver($driver)) {\n            return $resolver($connection, $database, $prefix, $config);\n        }\n\n        switch ($driver) {\n            case 'mysql':\n                return new MySqlConnection($connection, $database, $prefix, $config);\n            case 'mariadb':\n                return new MariaDbConnection($connection, $database, $prefix, $config);\n            case 'pgsql':\n                return new PostgresConnection($connection, $database, $prefix, $config);\n            case 'sqlite':\n                return new SQLiteConnection($connection, $database, $prefix, $config);\n            case 'sqlsrv':\n                return new SqlServerConnection($connection, $database, $prefix, $config);\n        }\n\n        throw new InvalidArgumentException(\"Unsupported driver [$driver]\");\n    }\n}\n"
  },
  {
    "path": "src/Database/DatabaseServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Database;\n\nuse October\\Rain\\Database\\Updater;\nuse October\\Rain\\Database\\Schema\\Blueprint;\nuse October\\Rain\\Database\\Connectors\\ConnectionFactory;\nuse Illuminate\\Database\\DatabaseServiceProvider as DatabaseServiceProviderBase;\nuse Illuminate\\Database\\DatabaseManager;\nuse Illuminate\\Database\\DatabaseTransactionsManager;\nuse Illuminate\\Database\\ConcurrencyErrorDetector;\nuse Illuminate\\Database\\LostConnectionDetector;\nuse Illuminate\\Contracts\\Database\\ConcurrencyErrorDetector as ConcurrencyErrorDetectorContract;\nuse Illuminate\\Contracts\\Database\\LostConnectionDetector as LostConnectionDetectorContract;\n\n/**\n * DatabaseServiceProvider\n */\nclass DatabaseServiceProvider extends DatabaseServiceProviderBase\n{\n    /**\n     * register the service provider\n     */\n    public function register()\n    {\n        Model::clearBootedModels();\n\n        $this->registerConnectionServices();\n        $this->registerFakerGenerator();\n        $this->registerQueueableEntityResolver();\n    }\n\n    /**\n     * boot the application events\n     */\n    public function boot()\n    {\n        Model::setConnectionResolver($this->app['db']);\n        Model::setEventDispatcher($this->app['events']);\n    }\n\n    /**\n     * registerConnectionServices for the primary database bindings.\n     */\n    protected function registerConnectionServices()\n    {\n        // The connection factory is used to create the actual connection instances on\n        // the database. We will inject the factory into the manager so that it may\n        // make the connections while they are actually needed and not of before.\n        $this->app->singleton('db.factory', function ($app) {\n            return new ConnectionFactory($app);\n        });\n\n        // The database manager is used to resolve various connections, since multiple\n        // connections might be managed. It also implements the connection resolver\n        // interface which may be used by other components requiring connections.\n        $this->app->singleton('db', function ($app) {\n            return new DatabaseManager($app, $app['db.factory']);\n        });\n\n        $this->app->bind('db.connection', function ($app) {\n            return $app['db']->connection();\n        });\n\n        $this->app->bind('db.schema', function ($app) {\n            $builder = $app['db']->connection()->getSchemaBuilder();\n\n            // Custom blueprint resolver for schema\n            $builder->blueprintResolver(function ($connection, $table, $callback) {\n                return new Blueprint($connection, $table, $callback);\n            });\n\n            return $builder;\n        });\n\n        $this->app->singleton('db.transactions', function ($app) {\n            return new DatabaseTransactionsManager;\n        });\n\n        $this->app->bind('db.replicator', Replicator::class);\n\n        $this->app->singleton('db.dongle', function ($app) {\n            return new Dongle($this->getDefaultDatabaseDriver(), $app['db']);\n        });\n\n        $this->app->singleton('db.updater', function ($app) {\n            return new Updater;\n        });\n\n        $this->app->singleton(ConcurrencyErrorDetectorContract::class, ConcurrencyErrorDetector::class);\n\n        $this->app->singleton(LostConnectionDetectorContract::class, LostConnectionDetector::class);\n    }\n\n    /**\n     * getDefaultDatabaseDriver returns the default database driver, not just the connection name\n     */\n    protected function getDefaultDatabaseDriver(): string\n    {\n        $defaultConnection = $this->app['db']->getDefaultConnection();\n\n        return $this->app['config'][\"database.connections.{$defaultConnection}.driver\"];\n    }\n}\n"
  },
  {
    "path": "src/Database/Dongle.php",
    "content": "<?php namespace October\\Rain\\Database;\n\n/**\n * Dongle driver for database that uses regex to convert MySQL to various other drivers.\n */\nclass Dongle\n{\n    /**\n     * @var \\Db db helper object\n     */\n    protected $db;\n\n    /**\n     * @var string driver to convert to: mysql, sqlite, pgsql, sqlsrv, postgis\n     */\n    protected $driver;\n\n    /**\n     * @var bool strictModeDisabled used to determine whether strict mode has been disabled\n     */\n    protected $strictModeDisabled;\n\n    /**\n     * __construct\n     */\n    public function __construct($driver = 'mysql', $db = null)\n    {\n        $this->db = $db;\n        $this->driver = $driver;\n    }\n\n    /**\n     * raw transforms and executes a raw SQL statement\n     */\n    public function raw(string $sql, ?array $params = null)\n    {\n        return $this->db->raw($this->parse($sql, $params));\n    }\n\n    /**\n     * rawValue converts a raw expression to a string\n     *\n     * @todo Can be refactored if Laravel >= 10\n     */\n    public function rawValue($sql): string\n    {\n        if (interface_exists(\\Illuminate\\Contracts\\Database\\Query\\Expression::class)) {\n            return $this->db->raw($sql)->getValue($this->db->connection()->getQueryGrammar());\n        }\n\n        return (string) $this->db->raw($sql);\n    }\n\n    /**\n     * parse transforms an SQL statement to match the active driver. If params are supplied,\n     * replaces :column_name with array value without requiring a list of names.\n     * Example: custom_country_id = :country_id → custom_country_id = 7\n     */\n    public function parse(string $sql, ?array $params = null): string\n    {\n        if (is_array($params) && preg_match_all('/\\:([\\w]+)/', $sql, $matches)) {\n            $sql = $this->parseValues($sql, $params, $matches[1]);\n        }\n\n        $sql = $this->parseGroupConcat($sql);\n\n        $sql = $this->parseConcat($sql);\n\n        $sql = $this->parseIfNull($sql);\n\n        $sql = $this->parseGreatest($sql);\n\n        $sql = $this->parseBooleanExpression($sql);\n\n        return $sql;\n    }\n\n    /**\n     * parseValues will protect parameter values by quoting them or handling safe values. Eg:\n     *\n     *     username = :value   → username = 'foobar'\n     *     username = :value%  → username = 'foobar%'\n     *     username = %:value  → username = '%foobar'\n     *     username = %:value% → username = '%foobar%'\n     */\n    public function parseValues(string $sql, array $data, array $paramNames)\n    {\n        $toReplace = [];\n\n        foreach ($paramNames as $param) {\n            $parsedValue = array_key_exists($param, $data) ? $data[$param] : null;\n\n            if (is_string($parsedValue)) {\n                $pdo = $this->db->getPdo();\n                $toReplace['%:'.$param.'%'] = $pdo->quote('%'.$parsedValue.'%');\n                $toReplace['%:'.$param] = $pdo->quote('%'.$parsedValue);\n                $toReplace[':'.$param.'%'] = $pdo->quote($parsedValue.'%');\n                $toReplace[':'.$param] = $pdo->quote($parsedValue);\n            }\n            else {\n                if (is_null($parsedValue)) {\n                    $parsedValue = 'NULL';\n                }\n                elseif (is_numeric($parsedValue)) {\n                    $parsedValue = +$parsedValue;\n                }\n                else {\n                    $parsedValue = \"''\";\n                }\n\n                $toReplace['%:'.$param.'%'] = $parsedValue;\n                $toReplace['%:'.$param] = $parsedValue;\n                $toReplace[':'.$param.'%'] = $parsedValue;\n                $toReplace[':'.$param] = $parsedValue;\n            }\n        }\n\n        return strtr($sql, $toReplace);\n    }\n\n    /**\n     * parseGroupConcat transforms GROUP_CONCAT statement\n     */\n    public function parseGroupConcat(string $sql): string\n    {\n        if ($this->driver === 'mysql') {\n            return $sql;\n        }\n\n        if (!str_contains(strtolower($sql), 'group_concat(')) {\n            return $sql;\n        }\n\n        $result = preg_replace_callback('/group_concat\\((.+)\\)/i', function ($matches) {\n            if (!isset($matches[1])) {\n                return $matches[0];\n            }\n\n            switch ($this->driver) {\n                default:\n                    return $matches[0];\n\n                case 'pgsql':\n                case 'postgis':\n                case 'sqlite':\n                case 'sqlsrv':\n                    return str_ireplace(' separator ', ', ', $matches[0]);\n            }\n        }, $sql);\n\n        if ($this->driver === 'pgsql' || $this->driver === 'postgis') {\n            // @todo this leaks to other definitions\n            $result = preg_replace(\"/\\\\(([]a-zA-Z\\\\-\\\\_\\\\.]+)\\\\,/i\", \"($1::VARCHAR,\", $result);\n            $result = str_ireplace('group_concat(', 'string_agg(', $result);\n        }\n\n        // Requires https://groupconcat.codeplex.com/\n        if ($this->driver === 'sqlsrv') {\n            $result = str_ireplace('group_concat(', 'dbo.GROUP_CONCAT_D(', $result);\n        }\n\n        return $result;\n    }\n\n    /**\n     * parseConcat transforms CONCAT statement\n     */\n    public function parseConcat(string $sql): string\n    {\n        if ($this->driver === 'mysql') {\n            return $sql;\n        }\n\n        if (!str_contains(strtolower($sql), 'concat(')) {\n            return $sql;\n        }\n\n        // Pre process special characters inside quotes\n        $charComma = 'X___COMMA_CHAR___X';\n        $result = preg_replace_callback(\"/'(.*?[^\\\\\\\\])'/i\", function ($matches) use ($charComma) {\n            return str_replace(',', $charComma, $matches[0]);\n        }, $sql);\n\n        // Convert concat() to pipe (||) syntax\n        $result = preg_replace_callback('/(?:group_)?concat\\((.+)\\)(?R)?/i', function ($matches) {\n            if (!isset($matches[1])) {\n                return $matches[0];\n            }\n\n            // This is a group_concat() so ignore it\n            if (strpos($matches[0], 'group_') === 0) {\n                return $matches[0];\n            }\n\n            $concatFields = array_map('trim', explode(',', $matches[1]));\n\n            switch ($this->driver) {\n                default:\n                    return $matches[0];\n\n                case 'pgsql':\n                case 'postgis':\n                case 'sqlite':\n                    return implode(' || ', $concatFields);\n            }\n        }, $result);\n\n        // Replace special characters back to their originals\n        $result = str_replace($charComma, ',', $result);\n\n        return $result;\n    }\n\n    /**\n     * parseIfNull transforms IFNULL statement\n     */\n    public function parseIfNull(string $sql): string\n    {\n        if ($this->driver === 'mysql') {\n            return $sql;\n        }\n\n        if (!str_contains(strtolower($sql), 'ifnull(')) {\n            return $sql;\n        }\n\n        if ($this->driver === 'pgsql' || $this->driver === 'postgis') {\n            return str_ireplace('ifnull(', 'coalesce(', $sql);\n        }\n\n        if ($this->driver === 'sqlsrv') {\n            return str_ireplace('ifnull(', 'isnull(', $sql);\n        }\n\n        return $sql;\n    }\n\n    /**\n     * parseGreatest transforms GREATEST statement for SQLite which does not\n     * support this function natively. Uses MAX() as a scalar replacement.\n     */\n    public function parseGreatest(string $sql): string\n    {\n        if ($this->driver !== 'sqlite') {\n            return $sql;\n        }\n\n        if (!str_contains(strtolower($sql), 'greatest(')) {\n            return $sql;\n        }\n\n        return str_ireplace('greatest(', 'max(', $sql);\n    }\n\n    /**\n     * parseBooleanExpression transforms true|false expressions in a statement\n     */\n    public function parseBooleanExpression(string $sql): string\n    {\n        if ($this->driver !== 'sqlite' && $this->driver !== 'sqlsrv') {\n            return $sql;\n        }\n\n        return preg_replace_callback('/(\\w+)\\s*(=|<>)\\s*(true|false)($|\\s)/i', function ($matches) {\n            array_shift($matches);\n            $space = array_pop($matches);\n            $matches[2] = $matches[2] === 'true' ? 1 : 0;\n            return implode(' ', $matches) . $space;\n        }, $sql);\n    }\n\n    /**\n     * cast for some drivers that require same-type comparisons\n     */\n    public function cast(string $sql, $asType = 'INTEGER'): string\n    {\n        if ($this->driver !== 'pgsql' && $this->driver !== 'postgis') {\n            return $sql;\n        }\n\n        return 'CAST('.$sql.' AS '.$asType.')';\n    }\n\n    /**\n     * convertTimestamps alters a table's TIMESTAMP field(s) to be nullable and converts existing values.\n     *\n     * This is needed to transition from older Laravel code that set DEFAULT 0, which is an\n     * invalid date in newer MySQL versions where NO_ZERO_DATE is included in strict mode.\n     *\n     * @param string        $table\n     * @param string|array  $columns Column name(s). Defaults to ['created_at', 'updated_at']\n     */\n    public function convertTimestamps($table, $columns = null)\n    {\n        if ($this->driver !== 'mysql') {\n            return;\n        }\n\n        if (!is_array($columns)) {\n            $columns = is_null($columns) ? ['created_at', 'updated_at'] : [$columns];\n        }\n\n        $prefixedTable = $this->getTablePrefix() . $table;\n\n        foreach ($columns as $column) {\n            $this->db->statement(\"ALTER TABLE {$prefixedTable} MODIFY `{$column}` TIMESTAMP NULL DEFAULT NULL\");\n            $this->db->update(\"UPDATE {$prefixedTable} SET {$column} = null WHERE {$column} = 0\");\n        }\n    }\n\n    /**\n     * disableStrictMode is used to disable strict mode during migration\n     */\n    public function disableStrictMode()\n    {\n        if ($this->driver !== 'mysql') {\n            return;\n        }\n\n        if ($this->strictModeDisabled || $this->db->getConfig('strict') === false) {\n            return;\n        }\n\n        $this->db->statement(\"SET @@SQL_MODE=''\");\n        $this->strictModeDisabled = true;\n    }\n\n    /**\n     * getDriver returns the driver name as a string, eg: pgsql\n     */\n    public function getDriver()\n    {\n        return $this->driver;\n    }\n\n    /**\n     * getTablePrefix gets the table prefix\n     */\n    public function getTablePrefix(): string\n    {\n        return $this->db->getTablePrefix();\n    }\n\n    /**\n     * @deprecated use parse with second argument\n     */\n    public function parseParams(string $sql, array $params)\n    {\n        return $this->parse($sql, $params);\n    }\n}\n"
  },
  {
    "path": "src/Database/ExpandoModel.php",
    "content": "<?php namespace October\\Rain\\Database;\n\n/**\n * ExpandoModel treats all attributes as dynamic that are serialized to a single JSON column\n * in the database. This is useful for settings and user preference model base classes.\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ExpandoModel extends Model\n{\n    /**\n     * @var string expandoColumn name to store the data\n     */\n    protected $expandoColumn = 'value';\n\n    /**\n     * @var array expandoPassthru attributes that should not be serialized\n     */\n    protected $expandoPassthru = [];\n\n    /**\n     * __construct\n     */\n    public function __construct(array $attributes = [])\n    {\n        parent::__construct($attributes);\n\n        $this->bindEvent('model.afterFetch', [$this, 'expandoAfterFetch']);\n\n        $this->bindEvent('model.afterSave', [$this, 'expandoAfterSave']);\n\n        // Process attributes last for traits with attribute modifiers\n        $this->bindEvent('model.beforeSaveDone', [$this, 'expandoBeforeSaveDone'], -1);\n\n        $this->addJsonable($this->expandoColumn);\n    }\n\n    /**\n     * setExpandoAttributes on the model and protects the passthru values\n     */\n    public function setExpandoAttributes(array $attributes = [])\n    {\n        $this->attributes = array_merge(\n            $this->attributes,\n            array_diff_key($attributes, array_flip($this->getExpandoPassthru()))\n        );\n    }\n\n    /**\n     * expandoAfterFetch constructor event\n     */\n    public function expandoAfterFetch()\n    {\n        $this->attributes = array_merge((array) $this->{$this->expandoColumn}, $this->attributes);\n\n        $this->syncOriginal();\n    }\n\n    /**\n     * expandoBeforeSaveDone constructor event\n     */\n    public function expandoBeforeSaveDone()\n    {\n        $this->{$this->expandoColumn} = array_diff_key(\n            $this->attributes,\n            array_flip($this->getExpandoPassthru())\n        );\n\n        $this->attributes = array_diff_key($this->attributes, $this->{$this->expandoColumn});\n    }\n\n    /**\n     * expandoAfterSave constructor event\n     */\n    public function expandoAfterSave()\n    {\n        $this->attributes = array_merge($this->{$this->expandoColumn}, $this->attributes);\n    }\n\n    /**\n     * getExpandoPassthru\n     */\n    protected function getExpandoPassthru()\n    {\n        $defaults = [\n            $this->expandoColumn,\n            $this->getKeyName(),\n            $this->getCreatedAtColumn(),\n            $this->getUpdatedAtColumn(),\n            'site_root_id',\n            'updated_user_id',\n            'created_user_id'\n        ];\n\n        return array_merge($defaults, $this->expandoPassthru);\n    }\n}\n"
  },
  {
    "path": "src/Database/Factories/Factory.php",
    "content": "<?php namespace October\\Rain\\Database\\Factories;\n\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory as FactoryBase;\n\n/**\n * Factory umbrella class\n */\nabstract class Factory extends FactoryBase\n{\n}\n"
  },
  {
    "path": "src/Database/Factories/HasFactory.php",
    "content": "<?php namespace October\\Rain\\Database\\Factories;\n\n/**\n * HasFactory implements factory support for a model\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges, Samuell\n */\ntrait HasFactory\n{\n    /**\n     * factory gets a new factory instance for the model.\n     *\n     * @param  callable|array|int|null  $count\n     * @param  callable|array  $state\n     * @return \\Illuminate\\Database\\Eloquent\\Factories\\Factory<static>\n     */\n    public static function factory($count = null, $state = [])\n    {\n        $factory = static::newFactory() ?: static::factoryForModel(get_called_class());\n\n        return $factory\n            ->count(is_numeric($count) ? $count : null)\n            ->state(is_callable($count) || is_array($count) ? $count : $state);\n    }\n\n    /**\n     * factoryForModel guesses a factory class based on the model class\n     */\n    protected static function factoryForModel(string $modelName)\n    {\n        if (strpos($modelName, 'App\\\\') === 0) {\n            $factory = str_replace('Models\\\\', 'Database\\\\Factories\\\\', $modelName) . 'Factory';\n        }\n        else {\n            $factory = str_replace('Models\\\\', 'Updates\\\\Factories\\\\', $modelName) . 'Factory';\n        }\n\n        return $factory::new();\n    }\n\n    /**\n     * newFactory creates a new factory instance for the model.\n     *\n     * @return \\Illuminate\\Database\\Eloquent\\Factories\\Factory<static>\n     */\n    protected static function newFactory()\n    {\n        //\n    }\n}\n"
  },
  {
    "path": "src/Database/Migrations/2013_10_01_000001_Db_Deferred_Bindings.php",
    "content": "<?php\n\nuse October\\Rain\\Database\\Schema\\Blueprint;\nuse October\\Rain\\Database\\Updates\\Migration;\n\nreturn new class extends Migration\n{\n    public function up()\n    {\n        Schema::create('deferred_bindings', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('master_type');\n            $table->string('master_field');\n            $table->string('slave_type');\n            $table->integer('slave_id');\n            $table->string('session_key');\n            $table->mediumText('pivot_data')->nullable();\n            $table->boolean('is_bind')->default(true);\n            $table->integer('sort_order')->nullable();\n            $table->timestamps();\n        });\n    }\n\n    public function down()\n    {\n        Schema::dropIfExists('deferred_bindings');\n    }\n};\n"
  },
  {
    "path": "src/Database/Migrations/2013_10_01_000002_Db_Files.php",
    "content": "<?php\n\nuse October\\Rain\\Database\\Schema\\Blueprint;\nuse October\\Rain\\Database\\Updates\\Migration;\n\nreturn new class extends Migration\n{\n    public function up()\n    {\n        Schema::create('files', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('disk_name');\n            $table->string('file_name');\n            $table->integer('file_size');\n            $table->string('content_type');\n            $table->string('title')->nullable();\n            $table->text('description')->nullable();\n            $table->string('field')->nullable();\n            $table->integer('attachment_id')->nullable();\n            $table->string('attachment_type')->nullable();\n            $table->boolean('is_public')->default(true);\n            $table->integer('sort_order')->nullable();\n            $table->timestamps();\n\n            $table->index(['attachment_type', 'attachment_id', 'field'], 'files_master_index');\n        });\n    }\n\n    public function down()\n    {\n        Schema::dropIfExists('files');\n    }\n};\n"
  },
  {
    "path": "src/Database/Migrations/2015_10_01_000003_Db_Revisions.php",
    "content": "<?php\n\nuse October\\Rain\\Database\\Schema\\Blueprint;\nuse October\\Rain\\Database\\Updates\\Migration;\n\nreturn new class extends Migration\n{\n    public function up()\n    {\n        Schema::create('revisions', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('revisionable_type');\n            $table->integer('revisionable_id');\n            $table->integer('user_id')->unsigned()->nullable()->index();\n            $table->string('field')->nullable()->index();\n            $table->string('cast')->nullable();\n            $table->text('old_value')->nullable();\n            $table->text('new_value')->nullable();\n            $table->timestamps();\n            $table->index(['revisionable_id', 'revisionable_type']);\n        });\n    }\n\n    public function down()\n    {\n        Schema::dropIfExists('revisions');\n    }\n};\n"
  },
  {
    "path": "src/Database/Migrations/2026_10_01_000004_Db_Translate_Attributes.php",
    "content": "<?php\n\nuse October\\Rain\\Database\\Schema\\Blueprint;\nuse October\\Rain\\Database\\Updates\\Migration;\n\nreturn new class extends Migration\n{\n    public function up()\n    {\n        Schema::create('translate_attributes', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('model_type', 512);\n            $table->integer('model_id');\n            $table->string('locale', 16);\n            $table->string('attribute', 128);\n            $table->mediumText('value')->nullable();\n            $table->index(\n                ['model_type', 'model_id', 'locale'],\n                'translate_type_id_locale_index'\n            );\n            $table->unique(\n                ['model_type', 'model_id', 'locale', 'attribute'],\n                'translate_unique_index'\n            );\n        });\n    }\n\n    public function down()\n    {\n        Schema::dropIfExists('translate_attributes');\n    }\n};\n"
  },
  {
    "path": "src/Database/Model.php",
    "content": "<?php namespace October\\Rain\\Database;\n\nuse Date;\nuse October\\Rain\\Support\\Arr;\nuse Illuminate\\Database\\Eloquent\\Model as EloquentModel;\nuse Illuminate\\Database\\Eloquent\\Collection as CollectionBase;\nuse Carbon\\CarbonInterface;\nuse InvalidArgumentException;\nuse DateTimeInterface;\nuse Exception;\n\n/**\n * Model is an active record base class that extends Eloquent with added\n * extendability and deferred bindings.\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Model extends EloquentModel\n{\n    use Concerns\\HasEvents;\n    use Concerns\\HasJsonable;\n    use Concerns\\HasAttributes;\n    use Concerns\\HasReplication;\n    use Concerns\\HasRelationships;\n    use \\October\\Rain\\Support\\Traits\\Emitter;\n    use \\October\\Rain\\Extension\\ExtendableTrait;\n    use \\October\\Rain\\Database\\Traits\\DeferredBinding;\n\n    /**\n     * @var array implement behaviors for this model.\n     */\n    public $implement;\n\n    /**\n     * @var array attributes are public so behaviors can modify them.\n     */\n    public $attributes = [];\n\n    /**\n     * @var array dates are attributes to convert to an instance of Carbon/DateTime objects.\n     */\n    protected $dates = [];\n\n    /**\n     * @var array savingOptions used by the {@link save()} method.\n     */\n    protected $savingOptions = [];\n\n    /**\n     * @var bool trimStrings will trim all string attributes of whitespace\n     */\n    public $trimStrings = true;\n\n    /**\n     * __construct\n     */\n    public function __construct(array $attributes = [])\n    {\n        parent::__construct();\n\n        $this->bootNicerEvents();\n\n        $this->extendableConstruct();\n\n        $this->initializeModelEvent();\n\n        $this->fill($attributes);\n    }\n\n    /**\n     * make a new model and return the instance\n     * @param array $attributes\n     * @return \\Illuminate\\Database\\Eloquent\\Model|static\n     */\n    public static function make($attributes = [])\n    {\n        return new static($attributes);\n    }\n\n    /**\n     * create a new model and return the instance.\n     * @param array $attributes\n     * @param string $sessionKey\n     * @return \\Illuminate\\Database\\Eloquent\\Model|static\n     */\n    public static function create(array $attributes = [], $sessionKey = null)\n    {\n        $model = new static($attributes);\n\n        $model->save(null, $sessionKey);\n\n        return $model;\n    }\n\n    /**\n     * reload the model attributes from the database.\n     * @return \\Illuminate\\Database\\Eloquent\\Model|static\n     */\n    public function reload()\n    {\n        if (!$this->exists) {\n            $this->syncOriginal();\n        }\n        elseif ($fresh = static::find($this->getKey())) {\n            $this->setRawAttributes($fresh->getAttributes(), true);\n        }\n\n        return $this;\n    }\n\n    /**\n     * @deprecated use unsetRelation or unsetRelations\n     */\n    public function reloadRelations($relationName = null)\n    {\n        if (!$relationName) {\n            $this->unsetRelations();\n        }\n        else {\n            $this->unsetRelation($relationName);\n        }\n    }\n\n    /**\n     * extend this object properties upon construction.\n     */\n    public static function extend(callable $callback)\n    {\n        self::extendableExtendCallback($callback);\n    }\n\n    /**\n     * newInstance creates a new instance of the given model.\n     * @param  array  $attributes\n     * @param  bool  $exists\n     * @return static\n     */\n    public function newInstance($attributes = [], $exists = false)\n    {\n        $model = parent::newInstance([], $exists);\n\n        /**\n         * @event model.newInstance\n         * Called when a new instance of a model is created\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.newInstance', function (\\October\\Rain\\Database\\Model $newModel) use (\\October\\Rain\\Database\\Model $model) {\n         *         // Transfer custom properties\n         *         $newModel->isLocked = $model->isLocked;\n         *     });\n         *\n         */\n        $this->fireEvent('model.newInstance', [$model]);\n\n        // Fill last so the above event can modify fillable\n        $model->fill((array) $attributes);\n\n        return $model;\n    }\n\n    /**\n     * newFromBuilder creates a new model instance that is existing.\n     * @param  array  $attributes\n     * @return \\Illuminate\\Database\\Eloquent\\Model|static\n     */\n    public function newFromBuilder($attributes = [], $connection = null)\n    {\n        $instance = $this->newInstance([], true);\n\n        if ($instance->fireModelEvent('fetching') === false) {\n            return $instance;\n        }\n\n        $instance->setRawAttributes((array) $attributes, true);\n\n        $instance->fireModelEvent('fetched', false);\n\n        $instance->setConnection($connection ?: $this->connection);\n\n        return $instance;\n    }\n\n    //\n    // Overrides\n    //\n\n    /**\n     * asDateTime returns a timestamp as DateTime object.\n     *\n     * @param  mixed  $value\n     * @return \\Carbon\\Carbon\n     */\n    protected function asDateTime($value)\n    {\n        if ($value instanceof CarbonInterface) {\n            return Date::instance($value);\n        }\n\n        if ($value instanceof DateTimeInterface) {\n            return Date::parse(\n                $value->format('Y-m-d H:i:s.u'),\n                $value->getTimezone()\n            );\n        }\n\n        if (is_numeric($value)) {\n            return Date::createFromTimestamp($value);\n        }\n\n        if ($this->isStandardDateFormat($value)) {\n            return Date::createFromFormat('Y-m-d', $value)->startOfDay();\n        }\n\n        $format = $this->getDateFormat();\n\n        try {\n            $date = Date::createFromFormat($format, $value);\n        }\n        catch (InvalidArgumentException $ex) {\n            $date = false;\n        }\n\n        return $date ?: Date::parse($value);\n    }\n\n    /**\n     * newEloquentBuilder for the model.\n     * @param  \\October\\Rain\\Database\\QueryBuilder $query\n     * @return \\October\\Rain\\Database\\Builder\n     */\n    public function newEloquentBuilder($query)\n    {\n        return new Builder($query);\n    }\n\n    /**\n     * newBaseQueryBuilder instance for the connection.\n     * @return \\October\\Rain\\Database\\QueryBuilder\n     */\n    protected function newBaseQueryBuilder()\n    {\n        $conn = $this->getConnection();\n\n        $grammar = $conn->getQueryGrammar();\n\n        $builder = new QueryBuilder($conn, $grammar, $conn->getPostProcessor());\n\n        return $builder;\n    }\n\n    /**\n     * newCollection instance.\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    public function newCollection(array $models = [])\n    {\n        return new Collection($models);\n    }\n\n    //\n    // Magic\n    //\n\n    /**\n     * __get\n     */\n    public function __get($name)\n    {\n        return $this->extendableGet($name);\n    }\n\n    /**\n     * __set\n     */\n    public function __set($name, $value)\n    {\n        return $this->extendableSet($name, $value);\n    }\n\n    /**\n     * __call\n     */\n    public function __call($name, $params)\n    {\n        // Never call handleRelation() anywhere else as it could\n        // break getRelationCaller(), use $this->{$name}() instead\n        if ($this->hasRelation($name)) {\n            return $this->handleRelation($name);\n        }\n\n        return $this->extendableCall($name, $params);\n    }\n\n    /**\n     * __isset determines if an attribute or relation exists on the model.\n     * @param  string  $key\n     * @return bool\n     */\n    public function __isset($key)\n    {\n        return !is_null($this->getAttribute($key));\n    }\n\n    //\n    // Pivot\n    //\n\n    /**\n     * newPivot as a generic pivot model instance.\n     * @param  \\October\\Rain\\Database\\Model  $parent\n     * @param  array  $attributes\n     * @param  string  $table\n     * @param  bool  $exists\n     * @param  string|null  $using\n     * @return \\October\\Rain\\Database\\Pivot\n     */\n    public function newPivot(EloquentModel $parent, array $attributes, $table, $exists, $using = null)\n    {\n        return $using\n            ? $using::fromRawAttributes($parent, $attributes, $table, $exists)\n            : Pivot::fromAttributes($parent, $attributes, $table, $exists);\n    }\n\n    /**\n     * newRelationPivot instance specific to a relation.\n     * @param  \\October\\Rain\\Database\\Model  $parent\n     * @param  string  $relationName\n     * @param  array   $attributes\n     * @param  string  $table\n     * @param  bool    $exists\n     * @return \\October\\Rain\\Database\\Pivot\n     */\n    public function newRelationPivot($relationName, $parent, $attributes, $table, $exists)\n    {\n        $definition = $this->getRelationDefinition($relationName);\n\n        if (!array_key_exists('pivotModel', $definition)) {\n            return;\n        }\n\n        return $this->newPivot($parent, $attributes, $table, $exists, $definition['pivotModel']);\n    }\n\n    //\n    // Saving\n    //\n\n    /**\n     * saveInternal is an internal method that saves the model to the database.\n     * This is used by {@link save()} and {@link forceSave()}.\n     * @param array $options\n     * @return bool\n     */\n    protected function saveInternal($options = [])\n    {\n        $this->savingOptions = $options;\n        $this->sessionKey = $options['sessionKey'] ?? null;\n\n        /**\n         * @event model.saveInternal\n         * Called before the model is saved\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.saveInternal', function ((array) $attributes, (array) $options) use (\\October\\Rain\\Database\\Model $model) {\n         *         // Prevent anything from saving ever!\n         *         return false;\n         *     });\n         *\n         */\n        if ($this->fireEvent('model.saveInternal', [$this->attributes, $options], true) === false) {\n            return false;\n        }\n\n        // Apply pre deferred bindings\n        if ($this->sessionKey !== null) {\n            $this->commitDeferredBefore($this->sessionKey);\n        }\n\n        // Save the record\n        $result = parent::save($options);\n\n        // Halted by event\n        if ($result === false) {\n            return $result;\n        }\n\n        // If there is nothing to update, Eloquent will not fire afterSave(),\n        // events should still fire for consistency.\n        if ($result === null) {\n            $this->fireModelEvent('updated', false);\n            $this->fireModelEvent('saved', false);\n        }\n\n        // Apply post deferred bindings\n        if ($this->sessionKey !== null) {\n            $this->commitDeferredAfter($this->sessionKey);\n        }\n\n        // After save deferred binding\n        $this->fireEvent('model.saveComplete');\n\n        return $result;\n    }\n\n    /**\n     * getSaveOption returns an option used while saving the model.\n     * @return mixed\n     */\n    public function getSaveOption($key, $default = null)\n    {\n        return $this->savingOptions[$key] ?? $default;\n    }\n\n    /**\n     * save the model to the database.\n     * @return bool\n     */\n    public function save(?array $options = [], $sessionKey = null)\n    {\n        return $this->saveInternal((array) $options + ['sessionKey' => $sessionKey]);\n    }\n\n    /**\n     * push saves the model and all of its relationships.\n     * @return bool\n     */\n    public function push(?array $options = [], $sessionKey = null)\n    {\n        $always = Arr::get($options, 'always', false);\n\n        if (!$this->save(null, $sessionKey) && !$always) {\n            return false;\n        }\n\n        foreach ($this->relations as $name => $models) {\n            if (!$this->isRelationPushable($name)) {\n                continue;\n            }\n\n            if ($models instanceof CollectionBase) {\n                $models = $models->all();\n            }\n            elseif ($models instanceof EloquentModel) {\n                $models = [$models];\n            }\n            else {\n                $models = (array) $models;\n            }\n\n            foreach (array_filter($models) as $model) {\n                if (!$model->push(null, $sessionKey)) {\n                    return false;\n                }\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * alwaysPush pushes the first level of relations even if the parent\n     * model has no changes.\n     * @return bool\n     */\n    public function alwaysPush(?array $options = [], $sessionKey = null)\n    {\n        return $this->push(['always' => true] + (array) $options, $sessionKey);\n    }\n\n    /**\n     * performDeleteOnModel performs the actual delete query on this model instance.\n     */\n    protected function performDeleteOnModel()\n    {\n        $this->performDeleteOnRelations();\n\n        $this->setKeysForSaveQuery($this->newQueryWithoutScopes())->delete();\n    }\n\n    /**\n     * __sleep prepare the object for serialization.\n     */\n    public function __sleep()\n    {\n        $this->unbindEvent();\n\n        $this->extendableDestruct();\n\n        return parent::__sleep();\n    }\n\n    /**\n     * __wakeup when a model is being unserialized, check if it needs to be booted.\n     */\n    public function __wakeup()\n    {\n        parent::__wakeup();\n\n        $this->bootNicerEvents();\n\n        $this->extendableConstruct();\n\n        $this->initializeModelEvent();\n    }\n}\n"
  },
  {
    "path": "src/Database/ModelBehavior.php",
    "content": "<?php namespace October\\Rain\\Database;\n\nuse October\\Rain\\Extension\\ExtensionBase;\n\n/**\n * Base class for model behaviors.\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ModelBehavior extends ExtensionBase\n{\n    /**\n     * @var \\October\\Rain\\Database\\Model Reference to the extended model.\n     */\n    protected $model;\n\n    /**\n     * Constructor\n     * @param \\October\\Rain\\Database\\Model $model The extended model.\n     */\n    public function __construct($model)\n    {\n        $this->model = $model;\n    }\n}\n"
  },
  {
    "path": "src/Database/ModelException.php",
    "content": "<?php namespace October\\Rain\\Database;\n\nuse October\\Rain\\Exception\\ValidationException;\n\n/**\n * ModelException is used when validation fails and contains the invalid model for easy analysis\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ModelException extends ValidationException\n{\n    /**\n     * @var Model model that is invalid\n     */\n    protected $model;\n\n    /**\n     * __construct receives the troublesome model\n     */\n    public function __construct(Model $model)\n    {\n        parent::__construct($model->errors());\n\n        $this->model = $model;\n    }\n\n    /**\n     * getModel returns the model with invalid attributes\n     */\n    public function getModel(): Model\n    {\n        return $this->model;\n    }\n}\n"
  },
  {
    "path": "src/Database/Models/DeferredBinding.php",
    "content": "<?php namespace October\\Rain\\Database\\Models;\n\nuse Event;\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Arr;\nuse October\\Rain\\Database\\Model;\nuse Throwable;\n\n/**\n * DeferredBinding Model\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass DeferredBinding extends Model\n{\n    use \\October\\Rain\\Database\\Traits\\Nullable;\n\n    /**\n     * @var string table associated with the model\n     */\n    public $table = 'deferred_bindings';\n\n    /**\n     * @var array jsonable attribute names that are json encoded and decoded from the database\n     */\n    protected $jsonable = ['pivot_data'];\n\n    /**\n     * @var array nullable attribute names which should be set to null when empty\n     */\n    protected $nullable = ['pivot_data'];\n\n    /**\n     * @var array hasDeferredCache is a cache for the hasDeferredActions check\n     */\n    protected static $hasDeferredCache = [];\n\n    /**\n     * beforeCreate prevents duplicates and conflicting binds\n     */\n    public function beforeCreate()\n    {\n        $existingRecord = $this->findBindingRecord();\n        if (!$existingRecord) {\n            return;\n        }\n\n        // Remove add-delete pairs\n        if ((bool) $this->is_bind !== (bool) $existingRecord->is_bind) {\n            $existingRecord->deleteCancel();\n            return false;\n        }\n\n        // Skip repeating bindings\n        return false;\n    }\n\n    /**\n     * getPivotDataForBind strips attributes beginning with an underscore, allowing\n     * meta data to be stored using the column alongside the data.\n     */\n    public function getPivotDataForBind($model, $relationName): array\n    {\n        $data = [];\n\n        foreach ((array) $this->pivot_data as $key => $value) {\n            if (str_starts_with($key, '_')) {\n                continue;\n            }\n\n            $data[$key] = $value;\n        }\n\n        if (\n            $model->isClassInstanceOf(\\October\\Contracts\\Database\\SortableRelationInterface::class) &&\n            $model->isSortableRelation($relationName)\n        ) {\n            $sortColumn = $model->getRelationSortOrderColumn($relationName);\n            $data[$sortColumn] = $this->sort_order;\n        }\n\n        return $data;\n    }\n\n    /**\n     * findBindingRecord finds a duplicate binding record\n     */\n    protected function findBindingRecord()\n    {\n        return self::where('master_type', $this->master_type)\n            ->where('master_field', $this->master_field)\n            ->where('slave_type', $this->slave_type)\n            ->where('slave_id', $this->slave_id)\n            ->where('session_key', $this->session_key)\n            ->first()\n        ;\n    }\n\n    /**\n     * hasDeferredActions allows efficient and informed checks used by validation\n     */\n    public static function hasDeferredActions($masterType, $sessionKey, $fieldName = null): bool\n    {\n        $cacheKey = \"{$masterType}.{$sessionKey}\";\n\n        if (!array_key_exists($cacheKey, self::$hasDeferredCache)) {\n            self::$hasDeferredCache[$cacheKey] = self::where('master_type', $masterType)\n                ->where('session_key', $sessionKey)\n                ->pluck('master_field')\n                ->all()\n            ;\n        }\n\n        if ($fieldName !== null) {\n            return in_array($fieldName, self::$hasDeferredCache[$cacheKey]);\n        }\n\n        return (bool) self::$hasDeferredCache[$cacheKey];\n    }\n\n    /**\n     * cancelDeferredActions cancels all deferred bindings to this model\n     */\n    public static function cancelDeferredActions($masterType, $sessionKey)\n    {\n        $records = self::where('master_type', $masterType)\n            ->where('session_key', $sessionKey)\n            ->get()\n        ;\n\n        foreach ($records as $record) {\n            $record->deleteCancel();\n        }\n    }\n\n    /**\n     * cleanUp orphan bindings\n     */\n    public static function cleanUp($days = 5)\n    {\n        $timestamp = Carbon::now()->subDays($days)->toDateTimeString();\n\n        $records = self::where('created_at', '<', $timestamp)->get();\n\n        foreach ($records as $record) {\n            $record->deleteCancel();\n        }\n    }\n\n    /**\n     * deleteCancel deletes this binding and cancel is actions\n     */\n    public function deleteCancel()\n    {\n        $this->deleteSlaveRecord();\n        $this->delete();\n    }\n\n    /**\n     * afterDelete\n     */\n    public function afterDelete()\n    {\n        self::$hasDeferredCache = [];\n    }\n\n    /**\n     * deleteSlaveRecord is logic to cancel a binding action\n     */\n    protected function deleteSlaveRecord()\n    {\n        if (!$this->is_bind) {\n            return;\n        }\n\n        // Try to delete unbound hasOne/hasMany records from the details table\n        try {\n            $masterType = $this->master_type;\n            $masterObject = new $masterType;\n\n            /**\n             * @event deferredBinding.newMasterInstance\n             * Called after the model is initialized when deleting the slave record\n             *\n             * Example usage:\n             *\n             *     $model->bindEvent('deferredBinding.newMasterInstance', function ((\\Model) $model) {\n             *         if ($model instanceof MyModel) {\n             *             $model->some_attribute = true;\n             *         }\n             *     });\n             *\n             */\n            if (\n                ($event = $this->fireEvent('deferredBinding.newMasterInstance', [$masterObject], true)) ||\n                ($event = Event::fire('deferredBinding.newMasterInstance', [$this, $masterObject], true))\n            ) {\n                $masterObject = $event;\n            }\n\n            if (!$masterObject->isDeferrable($this->master_field)) {\n                return;\n            }\n\n            $related = $masterObject->makeRelation($this->master_field);\n            $relatedObj = $related->find($this->slave_id);\n            if (!$relatedObj) {\n                return;\n            }\n\n            $options = $masterObject->getRelationDefinition($this->master_field);\n            if (!Arr::get($options, 'delete', false)) {\n                return;\n            }\n\n            // Only delete it if the relationship is null\n            $foreignKey = Arr::get($options, 'key', $masterObject->getForeignKey());\n            if ($foreignKey && !$relatedObj->$foreignKey) {\n                $relatedObj->delete();\n            }\n        }\n        catch (Throwable $ex) {\n            // Do nothing\n        }\n    }\n}\n"
  },
  {
    "path": "src/Database/Models/Revision.php",
    "content": "<?php namespace October\\Rain\\Database\\Models;\n\nuse October\\Rain\\Database\\Model;\n\n/**\n * Revision Model\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Revision extends Model\n{\n    /**\n     * @var string table associated with the model\n     */\n    public $table = 'revisions';\n\n    /**\n     * getNewValueAttribute returns \"new value\" casted as the saved type\n     */\n    public function getNewValueAttribute($value)\n    {\n        if ($value === null) {\n            return null;\n        }\n\n        if ($this->cast === 'date') {\n            return $this->asDateTime($value);\n        }\n\n        return $value;\n    }\n\n    /**\n     * getOldValueAttribute returns \"old value\" casted as the saved type\n     */\n    public function getOldValueAttribute($value)\n    {\n        if ($value === null) {\n            return null;\n        }\n\n        if ($this->cast === 'date') {\n            return $this->asDateTime($value);\n        }\n\n        return $value;\n    }\n}\n"
  },
  {
    "path": "src/Database/Models/TranslateAttribute.php",
    "content": "<?php namespace October\\Rain\\Database\\Models;\n\nuse October\\Rain\\Database\\Model;\n\n/**\n * TranslateAttribute stores translated attribute values for translatable models\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass TranslateAttribute extends Model\n{\n    /**\n     * @var string table associated with the model\n     */\n    public $table = 'translate_attributes';\n\n    /**\n     * @var bool timestamps\n     */\n    public $timestamps = false;\n\n    /**\n     * @var array fillable fields\n     */\n    protected $fillable = ['locale', 'attribute', 'value'];\n\n    /**\n     * @var array morphTo\n     */\n    public $morphTo = [\n        'model' => []\n    ];\n}\n"
  },
  {
    "path": "src/Database/MorphPivot.php",
    "content": "<?php namespace October\\Rain\\Database;\n\nuse October\\Rain\\Support\\Str;\n\n/**\n * MorphPivot\n *\n * This class is a carbon copy of Illuminate\\Database\\Eloquent\\Relations\\MorphPivot\n * so the base October\\Rain\\Database\\Pivot class can be inherited\n *\n * @see \\Illuminate\\Database\\Eloquent\\Relations\n */\nclass MorphPivot extends Pivot\n{\n    /**\n     * @var string morphType is the type of the polymorphic relation.\n     */\n    protected $morphType;\n\n    /**\n     * @var string morphClass is the value of the polymorphic relation.\n     */\n    protected $morphClass;\n\n    /**\n     * setKeysForSaveQuery sets the keys for a save update query\n     *\n     * @param  \\Illuminate\\Database\\Eloquent\\Builder  $query\n     * @return \\Illuminate\\Database\\Eloquent\\Builder\n     */\n    protected function setKeysForSaveQuery($query)\n    {\n        $query->where($this->morphType, $this->morphClass);\n\n        return parent::setKeysForSaveQuery($query);\n    }\n\n    /**\n     * setKeysForSelectQuery sets the keys for a select query.\n     *\n     * @param  \\Illuminate\\Database\\Eloquent\\Builder  $query\n     * @return \\Illuminate\\Database\\Eloquent\\Builder\n     */\n    protected function setKeysForSelectQuery($query)\n    {\n        $query->where($this->morphType, $this->morphClass);\n\n        return parent::setKeysForSelectQuery($query);\n    }\n\n    /**\n     * delete the pivot model record from the database.\n     *\n     * @return int\n     */\n    public function delete()\n    {\n        if (isset($this->attributes[$this->getKeyName()])) {\n            return (int) parent::delete();\n        }\n\n        if ($this->fireModelEvent('deleting') === false) {\n            return 0;\n        }\n\n        $query = $this->getDeleteQuery();\n\n        $query->where($this->morphType, $this->morphClass);\n\n        return tap($query->delete(), function () {\n            $this->fireModelEvent('deleted', false);\n        });\n    }\n\n    /**\n     * getMorphType for the pivot.\n     *\n     * @return string\n     */\n    public function getMorphType()\n    {\n        return $this->morphType;\n    }\n\n    /**\n     * setMorphType for the pivot\n     * @param  string  $morphType\n     * @return $this\n     */\n    public function setMorphType($morphType)\n    {\n        $this->morphType = $morphType;\n\n        return $this;\n    }\n\n    /**\n     * setMorphClass for the pivot\n     * @param  string  $morphClass\n     * @return \\Illuminate\\Database\\Eloquent\\Relations\\MorphPivot\n     */\n    public function setMorphClass($morphClass)\n    {\n        $this->morphClass = $morphClass;\n\n        return $this;\n    }\n\n\n    /**\n     * getQueueableId for the entity.\n     *\n     * @return mixed\n     */\n    public function getQueueableId()\n    {\n        if (isset($this->attributes[$this->getKeyName()])) {\n            return $this->getKey();\n        }\n\n        return sprintf(\n            '%s:%s:%s:%s:%s:%s',\n            $this->foreignKey, $this->getAttribute($this->foreignKey),\n            $this->relatedKey, $this->getAttribute($this->relatedKey),\n            $this->morphType, $this->morphClass\n        );\n    }\n\n    /**\n     * newQueryForRestoration for one or more models by their queueable IDs.\n     *\n     * @param  array|int  $ids\n     * @return \\Illuminate\\Database\\Eloquent\\Builder\n     */\n    public function newQueryForRestoration($ids)\n    {\n        if (is_array($ids)) {\n            return $this->newQueryForCollectionRestoration($ids);\n        }\n\n        if (!Str::contains($ids, ':')) {\n            return parent::newQueryForRestoration($ids);\n        }\n\n        $segments = explode(':', $ids);\n\n        return $this->newQueryWithoutScopes()\n            ->where($segments[0], $segments[1])\n            ->where($segments[2], $segments[3])\n            ->where($segments[4], $segments[5]);\n    }\n\n    /**\n     * newQueryForCollectionRestoration to restore multiple models by their queueable IDs.\n     *\n     * @param  array  $ids\n     * @return \\Illuminate\\Database\\Eloquent\\Builder\n     */\n    protected function newQueryForCollectionRestoration(array $ids)\n    {\n        $ids = array_values($ids);\n\n        if (!Str::contains($ids[0], ':')) {\n            return parent::newQueryForRestoration($ids);\n        }\n\n        $query = $this->newQueryWithoutScopes();\n\n        foreach ($ids as $id) {\n            $segments = explode(':', $id);\n\n            $query->orWhere(function ($query) use ($segments) {\n                return $query->where($segments[0], $segments[1])\n                    ->where($segments[2], $segments[3])\n                    ->where($segments[4], $segments[5]);\n            });\n        }\n\n        return $query;\n    }\n}\n"
  },
  {
    "path": "src/Database/NestedTreeScope.php",
    "content": "<?php namespace October\\Rain\\Database;\n\n/**\n * NestedTreeScope\n *\n * @deprecated\n * @see \\October\\Rain\\Database\\Scopes\\NestedTreeScope\n */\nclass NestedTreeScope extends \\October\\Rain\\Database\\Scopes\\NestedTreeScope\n{\n}\n"
  },
  {
    "path": "src/Database/Pivot.php",
    "content": "<?php namespace October\\Rain\\Database;\n\n/**\n * Pivot model base class\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Pivot extends Model\n{\n    use \\Illuminate\\Database\\Eloquent\\Relations\\Concerns\\AsPivot;\n\n    /**\n     * Indicates if the IDs are auto-incrementing.\n     *\n     * @var bool\n     */\n    public $incrementing = false;\n\n    /**\n     * The attributes that aren't mass assignable.\n     *\n     * @var array\n     */\n    protected $guarded = [];\n}\n"
  },
  {
    "path": "src/Database/QueryBuilder.php",
    "content": "<?php namespace October\\Rain\\Database;\n\nuse App;\nuse Carbon\\Carbon;\nuse Illuminate\\Database\\Query\\Builder as QueryBuilderBase;\n\n/**\n * QueryBuilder restores some features that were removed from base, it also\n * adds some new ones\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass QueryBuilder extends QueryBuilderBase\n{\n    /**\n     * @var string cacheKey is the key that should be used when caching the query\n     */\n    protected $cacheKey;\n\n    /**\n     * @var int cacheMinutes is the number of minutes to cache the query\n     */\n    protected $cacheMinutes;\n\n    /**\n     * @var array cacheTags is the tags for the query cache\n     */\n    protected $cacheTags;\n\n    /**\n     * Get an array with the values of a given column.\n     *\n     * @param  string  $column\n     * @param  string|null  $key\n     * @return array\n     */\n    public function lists($column, $key = null)\n    {\n        return $this->pluck($column, $key)->all();\n    }\n\n    /**\n     * Indicate that the query results should be cached.\n     *\n     * @param  \\DateTime|int  $minutes\n     * @param  string  $key\n     * @return $this\n     */\n    public function remember($minutes, $key = null)\n    {\n        $this->cacheMinutes = $minutes;\n        $this->cacheKey = $key;\n\n        return $this;\n    }\n\n    /**\n     * Indicate that the query results should be cached forever.\n     *\n     * @param  string  $key\n     * @return \\Illuminate\\Database\\Query\\Builder|static\n     */\n    public function rememberForever($key = null)\n    {\n        return $this->remember(-1, $key);\n    }\n\n    /**\n     * Indicate that the results, if cached, should use the given cache tags.\n     *\n     * @param  array|mixed  $cacheTags\n     * @return $this\n     */\n    public function cacheTags($cacheTags)\n    {\n        $this->cacheTags = $cacheTags;\n        return $this;\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function get($columns = ['*'])\n    {\n        if (!is_null($this->cacheMinutes)) {\n            return $this->getCached((array) $columns);\n        }\n\n        return parent::get($columns);\n    }\n\n    /**\n     * getCached executes the query as a cached \"select\" statement.\n     */\n    public function getCached(array $columns = ['*'])\n    {\n        if (is_null($this->columns)) {\n            $this->columns = $columns;\n        }\n\n        // If the query is requested to be cached, we will cache it using a unique key\n        // for this database connection and query statement, including the bindings\n        // that are used on this query, providing great convenience when caching.\n        [$key, $minutes] = $this->getCacheInfo();\n\n        $cache = $this->getCache();\n\n        $callback = $this->getCacheCallback($columns);\n\n        // If the \"minutes\" value is less than zero, we will use that as the indicator\n        // that the value should be remembered values should be stored indefinitely\n        // and if we have minutes we will use the typical remember function here.\n        if ($minutes < 0) {\n            $results = $cache->rememberForever($key, $callback);\n        }\n        else {\n            $expiresAt = Carbon::now()->addMinutes($minutes);\n            $results = $cache->remember($key, $expiresAt, $callback);\n        }\n\n        return collect($results);\n    }\n\n    /**\n     * Get the cache object with tags assigned, if applicable.\n     *\n     * @return \\Illuminate\\Cache\\CacheManager\n     */\n    protected function getCache()\n    {\n        $cache = App::make('cache');\n\n        return $this->cacheTags ? $cache->tags($this->cacheTags) : $cache;\n    }\n\n    /**\n     * getCacheInfo returns key and cache minutes\n     */\n    protected function getCacheInfo(): array\n    {\n        return [$this->getCacheKey(), $this->cacheMinutes];\n    }\n\n    /**\n     * getCacheKey returns a unique cache key for the complete query\n     */\n    public function getCacheKey(): string\n    {\n        return $this->cacheKey ?: $this->generateCacheKey();\n    }\n\n    /**\n     * Generate the unique cache key for the query.\n     *\n     * @return string\n     */\n    public function generateCacheKey()\n    {\n        $name = $this->connection->getName();\n\n        return md5($name.$this->toSql().serialize($this->getBindings()));\n    }\n\n    /**\n     * Get the Closure callback used when caching queries.\n     *\n     * @param  array  $columns\n     * @return \\Closure\n     */\n    protected function getCacheCallback($columns)\n    {\n        return function () use ($columns) {\n            return parent::get($columns)->all();\n        };\n    }\n\n    /**\n     * Retrieve the \"count\" result of the query,\n     * also strips off any orderBy clause.\n     *\n     * @param  string  $columns\n     * @return int\n     */\n    public function count($columns = '*')\n    {\n        $previousOrders = $this->orders;\n\n        $this->orders = null;\n\n        $result = parent::count($columns);\n\n        $this->orders = $previousOrders;\n\n        return $result;\n    }\n}\n"
  },
  {
    "path": "src/Database/README.md",
    "content": "## Rain Database\n\nThe October Rain Foundation is an extension of the Eloquent ORM used by Laravel. It adds the following features:\n\n### Usage Instructions\n\nSee the [Illuminate Database instructions](https://github.com/illuminate/database/blob/master/README.md) for usage outside the Laravel framework.\n\n### Alternate relations and events\n\nRelations and events can be defined using an alternative syntax, which is preferred by the [October CMS platform](http://octobercms.com).\n\n[See October CMS Model documentation](https://octobercms.com/docs/database/model)\n\n### Model validation\n\nModels can define validation rules Laravel's built-in Validator class.\n\n[See October CMS Model documentation](https://octobercms.com/docs/database/model)\n\n### Deferred bindings\n\nDeferred bindings allow you to postpone model relationships until the master record commits the changes. This is particularly useful if you need to prepare some models (such as file uploads) and associate them to another model that doesn't exist yet.\n\n[See Deferred binding documentation](https://octobercms.com/docs/database/relations#deferred-binding)\n\n### Tree Trait Interface\n\nTraits do not support interfaces so this cannot be executed in the code. These are the expectations of a \"Tree\" trait, currently: NestedTree, SimpleTree.\n\nThese methods should support query builder chaining, i.e defined as scopes:\n\n- getAllRoot(): Return just the root nodes.\n- getNested(): Return all nodes with the `children` relationship eager loaded.\n- listsNested(): Returns a key, value array of records, where values are indented based on their level.\n\nThese methods do not require chaining:\n\n- getChildren(): Return the child nodes below this one.\n- getChildCount(): Return the number of children below this node.\n\nAll models must return a collection of the base class `October\\Rain\\Database\\TreeCollection`.\n"
  },
  {
    "path": "src/Database/Relations/AttachMany.php",
    "content": "<?php namespace October\\Rain\\Database\\Relations;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphMany as MorphManyBase;\nuse Symfony\\Component\\HttpFoundation\\File\\UploadedFile;\nuse October\\Rain\\Database\\Attach\\File as FileModel;\n\n/**\n * AttachMany\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass AttachMany extends MorphManyBase\n{\n    use AttachOneOrMany;\n    use DefinedConstraints;\n\n    /**\n     * __construct a new has many relationship instance.\n     */\n    public function __construct(Builder $query, Model $parent, $type, $id, $isPublic, $localKey, $relationName = null)\n    {\n        $this->relationName = $relationName;\n\n        $this->public = $isPublic;\n\n        parent::__construct($query, $parent, $type, $id, $localKey);\n\n        $this->addDefinedConstraints();\n    }\n\n    /**\n     * setSimpleValue helper for setting this relationship using various expected\n     * values. For example, $model->relation = $value;\n     * @param mixed $value\n     * @return void\n     */\n    public function setSimpleValue($value)\n    {\n        // Nulling the relationship\n        if (!$value) {\n            $this->parent->unsetRelation($this->relationName);\n\n            if ($this->parent->exists) {\n                $this->parent->bindEventOnce('model.afterSave', function() {\n                    $this->ensureRelationIsEmpty();\n                });\n            }\n            return;\n        }\n\n        // Append a single newly uploaded file(s)\n        if ($value instanceof UploadedFile) {\n            $this->parent->bindEventOnce('model.afterSave', function () use ($value) {\n                $this->create(['data' => $value]);\n            });\n            return;\n        }\n\n        // Append existing File model\n        if ($value instanceof FileModel) {\n            $this->parent->bindEventOnce('model.afterSave', function () use ($value) {\n                $this->add($value);\n            });\n            return;\n        }\n\n        // Process multiple values\n        $files = $models = $keys = [];\n        if (is_array($value)) {\n            foreach ($value as $_value) {\n                if ($_value instanceof UploadedFile) {\n                    $files[] = $_value;\n                }\n                elseif ($_value instanceof FileModel) {\n                    $models[] = $_value;\n                }\n                elseif (is_numeric($_value)){\n                    $keys[] = $_value;\n                }\n            }\n        }\n\n        if ($files) {\n            $this->parent->bindEventOnce('model.afterSave', function () use ($files) {\n                foreach ($files as $file) {\n                    $this->create(['data' => $file]);\n                }\n            });\n        }\n\n        if ($keys) {\n            $this->parent->bindEventOnce('model.afterSave', function () use ($keys) {\n                $models = $this->getRelated()\n                    ->whereIn($this->getRelatedKeyName(), (array) $keys)\n                    ->get()\n                ;\n\n                foreach ($models as $model) {\n                    $this->add($model);\n                }\n            });\n        }\n\n        if ($models) {\n            $this->parent->bindEventOnce('model.afterSave', function () use ($models) {\n                foreach ($models as $model) {\n                    $this->add($model);\n                }\n            });\n        }\n    }\n\n    /**\n     * getSimpleValue helper for getting this relationship simple value,\n     * generally useful with form values.\n     * @return array|null\n     */\n    public function getSimpleValue()\n    {\n        $value = null;\n        $relationName = $this->relationName;\n\n        if ($this->parent->relationLoaded($relationName)) {\n            $files = $this->parent->getRelation($relationName);\n        }\n        else {\n            $files = $this->getResults();\n        }\n\n        if ($files) {\n            $value = [];\n            foreach ($files as $file) {\n                $value[] = $file->getKey();\n            }\n        }\n\n        return $value;\n    }\n}\n"
  },
  {
    "path": "src/Database/Relations/AttachOne.php",
    "content": "<?php namespace October\\Rain\\Database\\Relations;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphOne as MorphOneBase;\nuse Symfony\\Component\\HttpFoundation\\File\\UploadedFile;\nuse October\\Rain\\Database\\Attach\\File as FileModel;\n\n/**\n * AttachOne\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass AttachOne extends MorphOneBase\n{\n    use AttachOneOrMany;\n    use DefinedConstraints;\n\n    /**\n     * __construct a new has many relationship instance.\n     */\n    public function __construct(Builder $query, Model $parent, $type, $id, $isPublic, $localKey, $relationName = null)\n    {\n        $this->relationName = $relationName;\n\n        $this->public = $isPublic;\n\n        parent::__construct($query, $parent, $type, $id, $localKey);\n\n        $this->addDefinedConstraints();\n    }\n\n    /**\n     * setSimpleValue helper for setting this relationship using various expected\n     * values. For example, $model->relation = $value;\n     * @param mixed $value\n     * @return void\n     */\n    public function setSimpleValue($value)\n    {\n        if (is_array($value)) {\n            $value = current($value);\n        }\n\n        // Nulling the relationship\n        if (!$value) {\n            $this->parent->setRelation($this->relationName, null);\n\n            if ($this->parent->exists) {\n                $this->parent->bindEventOnce('model.afterSave', function() {\n                    $this->ensureRelationIsEmpty();\n                });\n            }\n            return;\n        }\n\n        // Newly uploaded file\n        if ($value instanceof UploadedFile) {\n            $this->parent->bindEventOnce('model.afterSave', function () use ($value) {\n                $file = $this->create(['data' => $value]);\n                $this->parent->setRelation($this->relationName, $file);\n            });\n        }\n        // Existing File model\n        elseif ($value instanceof FileModel) {\n            $this->parent->bindEventOnce('model.afterSave', function () use ($value) {\n                $this->add($value);\n            });\n        }\n        // Model key\n        elseif (is_numeric($value)) {\n            $this->parent->bindEventOnce('model.afterSave', function () use ($value) {\n                if ($model = $this->getRelated()->find($value)) {\n                    $this->add($model);\n                }\n            });\n        }\n\n        // The relation is set here to satisfy validation\n        $this->parent->setRelation($this->relationName, $value);\n\n        $this->parent->bindEventOnce('model.afterValidate', function() {\n            $this->parent->unsetRelation($this->relationName);\n        });\n    }\n\n    /**\n     * getSimpleValue helper for getting this relationship simple value,\n     * generally useful with form values.\n     * @return string|null\n     */\n    public function getSimpleValue()\n    {\n        $value = null;\n        $relationName = $this->relationName;\n\n        if ($this->parent->relationLoaded($relationName)) {\n            $file = $this->parent->getRelation($relationName);\n        }\n        else {\n            $file = $this->getResults();\n        }\n\n        if ($file) {\n            $value = $file->getKey();\n        }\n\n        return $value;\n    }\n}\n"
  },
  {
    "path": "src/Database/Relations/AttachOneOrMany.php",
    "content": "<?php namespace October\\Rain\\Database\\Relations;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Support\\Arr;\nuse October\\Rain\\Database\\Attach\\File as FileModel;\nuse Symfony\\Component\\HttpFoundation\\File\\UploadedFile;\n\n/**\n * AttachOneOrMany\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait AttachOneOrMany\n{\n    use DeferOneOrMany;\n\n    /**\n     * @var string relationName is the \"name\" of the relationship\n     */\n    protected $relationName;\n\n    /**\n     * @var bool public is a default value for file public or protected state\n     */\n    protected $public;\n\n    /**\n     * isPublic determines if the file should be flagged \"public\" or not\n     */\n    public function isPublic()\n    {\n        if (isset($this->public) && $this->public !== null) {\n            return $this->public;\n        }\n\n        return true;\n    }\n\n    /**\n     * addConstraints sets the field (relation name) constraint on the query\n     * @return void\n     */\n    public function addConstraints()\n    {\n        if (static::$constraints) {\n            $this->query->where($this->morphType, $this->morphClass);\n\n            $this->query->where($this->foreignKey, '=', $this->getParentKey());\n\n            $this->query->where('field', $this->relationName);\n\n            $this->query->whereNotNull($this->foreignKey);\n        }\n    }\n\n    /**\n     * getRelationExistenceQuery adds the constraints for a relationship count query\n     * @param  \\Illuminate\\Database\\Eloquent\\Builder  $query\n     * @param  \\Illuminate\\Database\\Eloquent\\Builder  $parentQuery\n     * @param  array|mixed  $columns\n     * @return \\Illuminate\\Database\\Eloquent\\Builder\n     */\n    public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])\n    {\n        if ($parentQuery->getQuery()->from === $query->getQuery()->from) {\n            $query = $this->getRelationExistenceQueryForSelfJoin($query, $parentQuery, $columns);\n        }\n        else {\n            $query = $query->select($columns)->whereColumn($this->getExistenceCompareKey(), '=', $this->getQualifiedParentKeyName());\n        }\n\n        $query = $query->where($this->morphType, $this->morphClass);\n\n        return $query->where('field', $this->relationName);\n    }\n\n    /**\n     * getRelationExistenceQueryForSelfRelation adds the constraints for a relationship query on the same table\n     * @param  \\Illuminate\\Database\\Eloquent\\Builder  $query\n     * @param  \\Illuminate\\Database\\Eloquent\\Builder  $parentQuery\n     * @param  array|mixed  $columns\n     * @return \\Illuminate\\Database\\Eloquent\\Builder\n     */\n    public function getRelationExistenceQueryForSelfRelation(Builder $query, Builder $parentQuery, $columns = ['*'])\n    {\n        $query->select($columns)->from(\n            $query->getModel()->getTable().' as '.$hash = $this->getRelationCountHash()\n        );\n\n        $query->getModel()->setTable($hash);\n\n        return $query->whereColumn($hash.'.'.$this->getForeignKeyName(), '=', $this->getQualifiedParentKeyName());\n    }\n\n    /**\n     * addEagerConstraints sets the field constraint for an eager load of the relation\n     * @param  array  $models\n     * @return void\n     */\n    public function addEagerConstraints(array $models)\n    {\n        parent::addEagerConstraints($models);\n\n        $this->query->where('field', $this->relationName);\n    }\n\n    /**\n     * addCommonEagerConstraints adds constraints without the field constraint, used to\n     * eager load multiple relations of a common type.\n     * @see \\October\\Rain\\Database\\Concerns\\HasEagerLoadAttachRelation\n     * @param  array  $models\n     * @return void\n     */\n    public function addCommonEagerConstraints(array $models)\n    {\n        parent::addEagerConstraints($models);\n    }\n\n    /**\n     * save the supplied related model\n     */\n    public function save(Model $model, $sessionKey = null)\n    {\n        if (!array_key_exists('is_public', $model->attributes)) {\n            $model->setAttribute('is_public', $this->isPublic());\n        }\n\n        $model->setAttribute('field', $this->relationName);\n\n        if ($sessionKey === null) {\n            $this->ensureAttachOneIsSingular();\n            return parent::save($model);\n        }\n\n        $this->add($model, $sessionKey);\n\n        return $model->save() ? $model : false;\n    }\n\n    /**\n     * saveQuietly saves the supplied related model without raising any events.\n     */\n    public function saveQuietly(Model $model, $sessionKey = null)\n    {\n        return Model::withoutEvents(function () use ($model, $sessionKey) {\n            return $this->save($model, $sessionKey);\n        });\n    }\n\n    /**\n     * saveMany saves multiple models with deferred binding support.\n     */\n    public function saveMany($models, $sessionKey = null)\n    {\n        foreach ($models as $model) {\n            $this->save($model, $sessionKey);\n        }\n\n        return $models;\n    }\n\n    /**\n     * saveManyQuietly saves multiple models without raising any events.\n     */\n    public function saveManyQuietly($models, $sessionKey = null)\n    {\n        return Model::withoutEvents(function () use ($models, $sessionKey) {\n            return $this->saveMany($models, $sessionKey);\n        });\n    }\n\n    /**\n     * create a new instance of this related model\n     */\n    public function create(array $attributes = [], $sessionKey = null)\n    {\n        if (!array_key_exists('is_public', $attributes)) {\n            $attributes = array_merge(['is_public' => $this->isPublic()], $attributes);\n        }\n\n        $attributes['field'] = $this->relationName;\n\n        if ($sessionKey === null) {\n            $this->ensureAttachOneIsSingular();\n        }\n\n        $model = parent::create($attributes);\n\n        if ($sessionKey !== null) {\n            $this->add($model, $sessionKey);\n        }\n\n        return $model;\n    }\n\n    /**\n     * createQuietly creates a new instance without raising any events.\n     */\n    public function createQuietly(array $attributes = [], $sessionKey = null)\n    {\n        return Model::withoutEvents(function () use ($attributes, $sessionKey) {\n            return $this->create($attributes, $sessionKey);\n        });\n    }\n\n    /**\n     * createMany creates multiple instances of related models.\n     */\n    public function createMany(iterable $records, $sessionKey = null)\n    {\n        $instances = [];\n\n        foreach ($records as $record) {\n            $instances[] = $this->create($record, $sessionKey);\n        }\n\n        return $instances;\n    }\n\n    /**\n     * createManyQuietly creates multiple instances without raising any events.\n     */\n    public function createManyQuietly(iterable $records, $sessionKey = null)\n    {\n        return Model::withoutEvents(function () use ($records, $sessionKey) {\n            return $this->createMany($records, $sessionKey);\n        });\n    }\n\n    /**\n     * createFromFile\n     */\n    public function createFromFile(string $filePath, array $attributes = [], $sessionKey = null)\n    {\n        if (!array_key_exists('is_public', $attributes)) {\n            $attributes = array_merge(['is_public' => $this->isPublic()], $attributes);\n        }\n\n        $attributes['field'] = $this->relationName;\n\n        if ($sessionKey === null) {\n            $this->ensureAttachOneIsSingular();\n        }\n\n        $model = parent::make($attributes);\n        $model->fromFile($filePath);\n        $model->save();\n\n        if ($sessionKey !== null) {\n            $this->add($model, $sessionKey);\n        }\n\n        return $model;\n    }\n\n    /**\n     * add a model to this relationship type\n     */\n    public function add(Model $model, $sessionKey = null)\n    {\n        if (!array_key_exists('is_public', $model->attributes)) {\n            $model->is_public = $this->isPublic();\n        }\n\n        if ($sessionKey === null) {\n            /**\n             * @event model.relation.beforeAdd\n             * Called before adding a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)\n             *\n             * Example usage:\n             *\n             *     $model->bindEvent('model.relation.beforeAdd', function (string $relationName, \\October\\Rain\\Database\\Model $relatedModel) use (\\October\\Rain\\Database\\Model $model) {\n             *         if ($relationName === 'some_relation') {\n             *             return false;\n             *         }\n             *     });\n             *\n             */\n            if ($this->parent->fireEvent('model.relation.beforeAdd', [$this->relationName, $model], true) === false) {\n                return;\n            }\n\n            $this->ensureAttachOneIsSingular();\n\n            // Associate the model\n            if ($this->parent->exists) {\n                $model->setAttribute($this->getForeignKeyName(), $this->getParentKey());\n                $model->setAttribute($this->getMorphType(), $this->morphClass);\n                $model->setAttribute('field', $this->relationName);\n                $model->save();\n            }\n            else {\n                $this->parent->bindEventOnce('model.afterSave', function () use ($model) {\n                    $model->setAttribute($this->getForeignKeyName(), $this->getParentKey());\n                    $model->setAttribute($this->getMorphType(), $this->morphClass);\n                    $model->setAttribute('field', $this->relationName);\n                    $model->save();\n                });\n            }\n\n            // Use the opportunity to set the relation in memory\n            if ($this instanceof AttachOne) {\n                $this->parent->setRelation($this->relationName, $model);\n            }\n            else {\n                $this->parent->unsetRelation($this->relationName);\n            }\n\n            /**\n             * @event model.relation.add\n             * Called after adding a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)\n             *\n             * Example usage:\n             *\n             *     $model->bindEvent('model.relation.add', function (string $relationName, \\October\\Rain\\Database\\Model $relatedModel) use (\\October\\Rain\\Database\\Model $model) {\n             *         $relatedClass = get_class($relatedModel);\n             *         $modelClass = get_class($model);\n             *         traceLog(\"{$relatedClass} was added as {$relationName} to {$modelClass}.\");\n             *     });\n             *\n             */\n            $this->parent->fireEvent('model.relation.add', [$this->relationName, $model]);\n        }\n        else {\n            $this->ensureAttachOneIsSingular($sessionKey);\n            $this->parent->bindDeferred($this->relationName, $model, $sessionKey);\n        }\n    }\n\n    /**\n     * addMany attaches an array of models to the parent instance with deferred binding support\n     * @param  array  $models\n     */\n    public function addMany($models, $sessionKey = null)\n    {\n        foreach ($models as $model) {\n            $this->add($model, $sessionKey);\n        }\n    }\n\n    /**\n     * remove a model from this relationship type\n     */\n    public function remove(Model $model, $sessionKey = null)\n    {\n        if ($sessionKey === null) {\n            /**\n             * @event model.relation.beforeRemove\n             * Called before removing a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)\n             *\n             * Example usage:\n             *\n             *     $model->bindEvent('model.relation.beforeRemove', function (string $relationName, \\October\\Rain\\Database\\Model $relatedModel) use (\\October\\Rain\\Database\\Model $model) {\n             *         if ($relationName === 'perm_relation') {\n             *             return false;\n             *         }\n             *     });\n             *\n             */\n            if ($this->parent->fireEvent('model.relation.beforeRemove', [$this->relationName, $model], true) === false) {\n                return;\n            }\n\n            if (!$this->isModelRemovable($model)) {\n                return;\n            }\n\n            $options = $this->parent->getRelationDefinition($this->relationName);\n\n            if (Arr::get($options, 'delete', false)) {\n                $model->delete();\n            }\n            else {\n                // Make this model an orphan ;~(\n                $model->setAttribute($this->getForeignKeyName(), null);\n                $model->setAttribute($this->getMorphType(), null);\n                $model->setAttribute('field', null);\n                $model->save();\n            }\n\n            // Use the opportunity to set the relation in memory\n            if ($this instanceof AttachOne) {\n                $this->parent->setRelation($this->relationName, null);\n            }\n            else {\n                $this->parent->unsetRelation($this->relationName);\n            }\n\n            /**\n             * @event model.relation.remove\n             * Called after removing a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)\n             *\n             * Example usage:\n             *\n             *     $model->bindEvent('model.relation.remove', function (string $relationName, \\October\\Rain\\Database\\Model $relatedModel) use (\\October\\Rain\\Database\\Model $model) {\n             *         $relatedClass = get_class($relatedModel);\n             *         $modelClass = get_class($model);\n             *         traceLog(\"{$relatedClass} was removed from {$modelClass}.\");\n             *     });\n             *\n             */\n            $this->parent->fireEvent('model.relation.remove', [$this->relationName, $model]);\n        }\n        else {\n            $this->parent->unbindDeferred($this->relationName, $model, $sessionKey);\n        }\n    }\n\n    /**\n     * isModelRemovable returns true if an existing model is already associated\n     */\n    protected function isModelRemovable($model): bool\n    {\n        return\n            ((string) $model->getAttribute($this->getForeignKeyName()) === (string) $this->getParentKey()) &&\n            $model->getAttribute($this->getMorphType()) === $this->morphClass &&\n            $model->getAttribute('field') === $this->relationName;\n    }\n\n    /**\n     * ensureAttachOneIsSingular ensures AttachOne only has one attachment,\n     * by deleting siblings for singular relations.\n     */\n    protected function ensureAttachOneIsSingular($sessionKey = null)\n    {\n        if (!$this instanceof AttachOne) {\n            return;\n        }\n\n        if ($sessionKey) {\n            foreach ($this->withDeferred($sessionKey)->get() as $record) {\n                $this->parent->unbindDeferred($this->relationName, $record, $sessionKey);\n            }\n            return;\n        }\n\n        if ($this->parent->exists) {\n            $this->delete();\n        }\n    }\n\n    /**\n     * @deprecated this method is removed in October CMS v4\n     */\n    public function makeValidationFile($value)\n    {\n        if ($value instanceof FileModel) {\n            $localPath = $value->getLocalPath();\n\n            // Exception handling for UploadedFile\n            if (file_exists($localPath)) {\n                return new UploadedFile(\n                    $localPath,\n                    $value->file_name,\n                    $value->content_type,\n                    null,\n                    true\n                );\n            }\n\n            // Fallback to string\n            $value = $localPath;\n        }\n\n        /*\n         * @todo `$value` might be a string, may not validate\n         */\n\n        return $value;\n    }\n\n    /**\n     * ensureRelationIsEmpty ensures the relation is empty, either deleted or nulled.\n     */\n    protected function ensureRelationIsEmpty()\n    {\n        $options = $this->parent->getRelationDefinition($this->relationName);\n\n        if (Arr::get($options, 'delete', false)) {\n            $this->delete();\n        }\n        else {\n            $this->update([$this->getForeignKeyName() => null]);\n        }\n    }\n\n    /**\n     * getRelatedKeyName\n     * @return string\n     */\n    public function getRelatedKeyName()\n    {\n        return $this->related->getKeyName();\n    }\n\n    /**\n     * @deprecated use getForeignKeyName\n     */\n    public function getForeignKey()\n    {\n        return $this->foreignKey;\n    }\n\n    /**\n     * @deprecated use getLocalKeyName\n     */\n    public function getOtherKey()\n    {\n        return $this->localKey;\n    }\n}\n"
  },
  {
    "path": "src/Database/Relations/BelongsTo.php",
    "content": "<?php namespace October\\Rain\\Database\\Relations;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsTo as BelongsToBase;\n\n/**\n * BelongsTo\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass BelongsTo extends BelongsToBase\n{\n    use DeferOneOrMany;\n    use DefinedConstraints;\n\n    /**\n     * @var string relationName is the \"name\" of the relationship\n     */\n    protected $relationName;\n\n    /**\n     * __construct a new belongs to relationship instance.\n     */\n    public function __construct(Builder $query, Model $child, $foreignKey, $ownerKey, $relationName)\n    {\n        $this->relationName = $relationName;\n\n        parent::__construct($query, $child, $foreignKey, $ownerKey, $relationName);\n\n        $this->addDefinedConstraints();\n    }\n\n    /**\n     * create a new instance of this related model with deferred binding support\n     */\n    public function create(array $attributes = [], $sessionKey = null)\n    {\n        $model = parent::create($attributes);\n\n        $this->add($model, $sessionKey);\n\n        return $model;\n    }\n\n    /**\n     * add a model to this relationship type.\n     */\n    public function add(Model $model, $sessionKey = null)\n    {\n        if ($sessionKey === null) {\n            $this->associate($model);\n        }\n        else {\n            $this->child->bindDeferred($this->relationName, $model, $sessionKey);\n        }\n    }\n\n    /**\n     * remove a model from this relationship type.\n     */\n    public function remove(Model $model, $sessionKey = null)\n    {\n        if ($sessionKey === null) {\n            $this->dissociate();\n        }\n        else {\n            $this->child->unbindDeferred($this->relationName, $model, $sessionKey);\n        }\n    }\n\n    /**\n     * associate the model instance to the given parent.\n     */\n    public function associate($model)\n    {\n        /**\n         * @event model.relation.beforeAssociate\n         * Called before associating a relation to the model (only for BelongsTo/MorphTo relations)\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.relation.beforeAssociate', function (string $relationName, \\October\\Rain\\Database\\Model $relatedModel) use (\\October\\Rain\\Database\\Model $model) {\n         *         if ($relationName === 'some_relation') {\n         *             return false;\n         *         }\n         *     });\n         *\n         */\n        if ($this->parent->fireEvent('model.relation.beforeAssociate', [$this->relationName, $model], true) === false) {\n            return;\n        }\n\n        $result = parent::associate($model);\n\n        /**\n         * @event model.relation.associate\n         * Called after associating a relation to the model (only for BelongsTo/MorphTo relations)\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.relation.associate', function (string $relationName, \\October\\Rain\\Database\\Model $relatedModel) use (\\October\\Rain\\Database\\Model $model) {\n         *         $relatedClass = get_class($relatedModel);\n         *         $modelClass = get_class($model);\n         *         traceLog(\"{$relatedClass} was associated as {$relationName} to {$modelClass}.\");\n         *     });\n         *\n         */\n        $this->parent->fireEvent('model.relation.associate', [$this->relationName, $model]);\n\n        return $result;\n    }\n\n    /**\n     * dissociate previously dissociated model from the given parent.\n     */\n    public function dissociate()\n    {\n        /**\n         * @event model.relation.beforeDissociate\n         * Called before dissociating a relation to the model (only for BelongsTo/MorphTo relations)\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.relation.beforeDissociate', function (string $relationName) use (\\October\\Rain\\Database\\Model $model) {\n         *         if ($relationName === 'perm_relation') {\n         *             return false;\n         *         }\n         *     });\n         *\n         */\n        if ($this->parent->fireEvent('model.relation.beforeDissociate', [$this->relationName], true) === false) {\n            return;\n        }\n\n        $result = parent::dissociate();\n\n        /**\n         * @event model.relation.dissociate\n         * Called after dissociating a relation to the model (only for BelongsTo/MorphTo relations)\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.relation.dissociate', function (string $relationName) use (\\October\\Rain\\Database\\Model $model) {\n         *         $modelClass = get_class($model);\n         *         traceLog(\"{$relationName} was dissociated from {$modelClass}.\");\n         *     });\n         *\n         */\n        $this->parent->fireEvent('model.relation.dissociate', [$this->relationName]);\n\n        return $result;\n    }\n\n    /**\n     * setSimpleValue helper for setting this relationship using various expected\n     * values. For example, $model->relation = $value;\n     * @param mixed $value\n     * @return void\n     */\n    public function setSimpleValue($value)\n    {\n        // Nulling the relationship\n        if (!$value) {\n            $this->dissociate();\n            return;\n        }\n\n        if ($value instanceof Model) {\n            // Non existent model, use a single serve event to associate it again when ready\n            if (!$value->exists) {\n                $value->bindEventOnce('model.afterSave', function () use ($value) {\n                    $this->associate($value);\n                });\n            }\n\n            $this->associate($value);\n            $this->child->setRelation($this->relationName, $value);\n        }\n        else {\n            $this->child->setAttribute($this->getForeignKeyName(), $value);\n            $this->child->unsetRelation($this->relationName);\n        }\n    }\n\n    /**\n     * getSimpleValue is a helper for getting this relationship simple value,\n     * generally useful with form values.\n     * @return string|null\n     */\n    public function getSimpleValue()\n    {\n        return $this->child->getAttribute($this->getForeignKeyName());\n    }\n\n    /**\n     * @deprecated use getOwnerKeyName\n     */\n    public function getOtherKey()\n    {\n        return $this->ownerKey;\n    }\n}\n"
  },
  {
    "path": "src/Database/Relations/BelongsToMany.php",
    "content": "<?php namespace October\\Rain\\Database\\Relations;\n\nuse Site;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Collection as CollectionBase;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany as BelongsToManyBase;\nuse October\\Rain\\Support\\Facades\\DbDongle;\n\n/**\n * BelongsToMany\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass BelongsToMany extends BelongsToManyBase\n{\n    use DeferOneOrMany;\n    use DefinedConstraints;\n    use \\October\\Rain\\Database\\Concerns\\HasNicerPagination;\n\n    /**\n     * @var bool countMode sets this relation object is a 'count' helper\n     * @deprecated use Laravel withCount() method instead\n     */\n    public $countMode = false;\n\n    /**\n     * __construct a new belongs to many relationship instance.\n     *\n     * @param  string  $table\n     * @param  string  $foreignPivotKey\n     * @param  string  $relatedPivotKey\n     * @param  string  $relationName\n     */\n    public function __construct(\n        Builder $query,\n        Model $parent,\n        $table,\n        $foreignPivotKey,\n        $relatedPivotKey,\n        $parentKey,\n        $relatedKey,\n        $relationName = null\n    ) {\n        parent::__construct(\n            $query,\n            $parent,\n            $table,\n            $foreignPivotKey,\n            $relatedPivotKey,\n            $parentKey,\n            $relatedKey,\n            $relationName\n        );\n\n        $this->addDefinedConstraints();\n    }\n\n    /**\n     * addWhereConstraints sets the where clause for the relation query.\n     * @return $this\n     */\n    protected function addWhereConstraints()\n    {\n        parent::addWhereConstraints();\n\n        $this->addPivotSiteScopeConstraints();\n\n        return $this;\n    }\n\n    /**\n     * addEagerConstraints sets the constraints for an eager load of the relation.\n     * @param  array  $models\n     * @return void\n     */\n    public function addEagerConstraints(array $models)\n    {\n        parent::addEagerConstraints($models);\n\n        $this->addPivotSiteScopeConstraints();\n    }\n\n    /**\n     * baseAttachRecord creates a new pivot attachment record.\n     * @param  int   $id\n     * @param  bool  $timed\n     * @return array\n     */\n    protected function baseAttachRecord($id, $timed)\n    {\n        $record = parent::baseAttachRecord($id, $timed);\n\n        if ($siteId = $this->getPivotSiteScopeValue()) {\n            $record['site_id'] = $siteId;\n        }\n\n        return $record;\n    }\n\n    /**\n     * getRelationExistenceQuery adds the constraints for a relationship count query.\n     * @param  \\Illuminate\\Database\\Eloquent\\Builder  $query\n     * @param  \\Illuminate\\Database\\Eloquent\\Builder  $parentQuery\n     * @param  array|mixed  $columns\n     * @return \\Illuminate\\Database\\Eloquent\\Builder\n     */\n    public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])\n    {\n        $query = parent::getRelationExistenceQuery($query, $parentQuery, $columns);\n\n        if ($this->pivotSiteScope) {\n            $query->where($this->qualifyPivotColumn('site_id'), Site::getSiteIdFromContext());\n        }\n\n        return $query;\n    }\n\n    /**\n     * newPivotQuery creates a new query builder for the pivot table.\n     */\n    public function newPivotQuery()\n    {\n        $query = parent::newPivotQuery();\n\n        if ($this->pivotSiteScope) {\n            $query->where($this->table.'.site_id', Site::getSiteIdFromContext());\n        }\n\n        return $query;\n    }\n\n    /**\n     * save the supplied related model with deferred binding support.\n     */\n    public function save(Model $model, array $pivotData = [], $sessionKey = null)\n    {\n        $model->save();\n\n        $this->add($model, $sessionKey, $pivotData);\n\n        return $model;\n    }\n\n    /**\n     * saveQuietly saves the model without raising any events,\n     * with deferred binding support.\n     */\n    public function saveQuietly(Model $model, array $pivotData = [], $sessionKey = null)\n    {\n        return Model::withoutEvents(function () use ($model, $pivotData, $sessionKey) {\n            return $this->save($model, $pivotData, $sessionKey);\n        });\n    }\n\n    /**\n     * saveMany saves multiple models with deferred binding support.\n     */\n    public function saveMany($models, array $pivotData = [], $sessionKey = null)\n    {\n        foreach ($models as $model) {\n            $this->save($model, $pivotData, $sessionKey);\n        }\n\n        return $models;\n    }\n\n    /**\n     * saveManyQuietly saves multiple models without raising any events,\n     * with deferred binding support.\n     */\n    public function saveManyQuietly($models, array $pivotData = [], $sessionKey = null)\n    {\n        return Model::withoutEvents(function () use ($models, $pivotData, $sessionKey) {\n            return $this->saveMany($models, $pivotData, $sessionKey);\n        });\n    }\n\n    /**\n     * create a new instance of this related model with deferred binding support.\n     */\n    public function create(array $attributes = [], array $pivotData = [], $sessionKey = null)\n    {\n        $model = $this->related->create($attributes);\n\n        $this->add($model, $sessionKey, $pivotData);\n\n        return $model;\n    }\n\n    /**\n     * createQuietly creates a new instance without raising any events,\n     * with deferred binding support.\n     */\n    public function createQuietly(array $attributes = [], array $pivotData = [], $sessionKey = null)\n    {\n        return Model::withoutEvents(function () use ($attributes, $pivotData, $sessionKey) {\n            return $this->create($attributes, $pivotData, $sessionKey);\n        });\n    }\n\n    /**\n     * createMany creates multiple related models with deferred binding support.\n     */\n    public function createMany(iterable $records, array $pivotData = [], $sessionKey = null)\n    {\n        $instances = $this->related->newCollection();\n\n        foreach ($records as $record) {\n            $instances->push($this->create($record, $pivotData, $sessionKey));\n        }\n\n        return $instances;\n    }\n\n    /**\n     * createManyQuietly creates multiple models without raising any events,\n     * with deferred binding support.\n     */\n    public function createManyQuietly(iterable $records, array $pivotData = [], $sessionKey = null)\n    {\n        return Model::withoutEvents(function () use ($records, $pivotData, $sessionKey) {\n            return $this->createMany($records, $pivotData, $sessionKey);\n        });\n    }\n\n    /**\n     * createOrFirst attempts to create the record, or if a unique constraint\n     * violation occurs, finds the existing record.\n     */\n    public function createOrFirst(array $attributes = [], \\Closure|array $values = [], array $pivotData = [], $sessionKey = null)\n    {\n        $model = $this->related->createOrFirst($attributes, $values);\n\n        $this->add($model, $sessionKey, $pivotData);\n\n        return $model;\n    }\n\n    /**\n     * attach overrides attach() method of BelongToMany relation\n     * This is necessary in order to fire 'model.relation.beforeAttach', 'model.relation.attach' events\n     * @param mixed $ids\n     * @param array $attributes\n     * @param bool  $touch\n     */\n    public function attach($ids, array $attributes = [], $touch = true)\n    {\n        // Normalize identifiers for events, this occurs internally in the parent logic\n        // and should have no cascading effects.\n        $parsedIds = $this->parseIds($ids);\n\n        /**\n         * @event model.relation.beforeAttach\n         * Called before creating a new relation between models (only for BelongsToMany relation)\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.relation.beforeAttach', function (string $relationName, array $ids, array $attributes) use (\\October\\Rain\\Database\\Model $model) {\n         *         foreach ($ids as $id) {\n         *             if (!$model->isRelationValid($id)) {\n         *                 return false;\n         *             }\n         *         }\n         *     });\n         *\n         */\n        if ($this->parent->fireEvent('model.relation.beforeAttach', [$this->relationName, &$parsedIds, &$attributes], true) === false) {\n            return;\n        }\n\n        /*\n         * See \\Illuminate\\Database\\Eloquent\\Relations\\Concerns\\InteractsWithPivotTable\n         */\n        parent::attach($parsedIds, $attributes, $touch);\n\n        /**\n         * @event model.relation.attach\n         * Called after creating a new relation between models (only for BelongsToMany relation)\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.relation.attach', function (string $relationName, array $ids, array $attributes) use (\\October\\Rain\\Database\\Model $model) {\n         *         foreach ($ids as $id) {\n         *             traceLog(\"New relation {$relationName} was created\", $id);\n         *         }\n         *     });\n         *\n         */\n        $this->parent->fireEvent('model.relation.attach', [$this->relationName, $parsedIds, $attributes]);\n    }\n\n    /**\n     * detach overrides detach() method of BelongToMany relation.\n     * This is necessary in order to fire 'model.relation.beforeDetach', 'model.relation.detach' events\n     * @param mixed $ids\n     * @param bool $touch\n     * @return int|void\n     */\n    public function detach($ids = null, $touch = true)\n    {\n        // Normalize identifiers for events, this occurs internally in the parent logic\n        // and should have no cascading effects. Null is used to detach everything.\n        $parsedIds = $ids !== null ? $this->parseIds($ids) : $ids;\n\n        /**\n         * @event model.relation.beforeDetach\n         * Called before removing a relation between models (only for BelongsToMany relation)\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.relation.beforeDetach', function (string $relationName, ?array $parsedIds) use (\\October\\Rain\\Database\\Model $model) {\n         *         foreach ((array) $parsedIds as $id) {\n         *             if (!$model->isRelationValid($parsedIds)) {\n         *                 return false;\n         *             }\n         *         }\n         *     });\n         *\n         */\n        if ($this->parent->fireEvent('model.relation.beforeDetach', [$this->relationName, &$parsedIds], true) === false) {\n            return;\n        }\n\n        /*\n         * See \\Illuminate\\Database\\Eloquent\\Relations\\Concerns\\InteractsWithPivotTable\n         */\n        $results = parent::detach($parsedIds, $touch);\n\n        /**\n         * @event model.relation.detach\n         * Called after removing a relation between models (only for BelongsToMany relation)\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.relation.detach', function (string $relationName, ?array $parsedIds, int $results) use (\\October\\Rain\\Database\\Model $model) {\n         *         foreach ($ids as $id) {\n         *             traceLog(\"Relation {$relationName} was removed\", (array) $parsedIds);\n         *         }\n         *     });\n         *\n         */\n        $this->parent->fireEvent('model.relation.detach', [$this->relationName, $parsedIds, $results]);\n    }\n\n    /**\n     * add a model to this relationship type.\n     */\n    public function add(Model $model, $sessionKey = null, $pivotData = [])\n    {\n        if (is_array($sessionKey)) {\n            $pivotData = $sessionKey;\n            $sessionKey = null;\n        }\n\n        // Associate the model\n        if ($sessionKey === null) {\n            if ($this->parent->exists) {\n                $this->attach($model, $pivotData);\n            }\n            else {\n                $this->parent->bindEventOnce('model.afterSave', function () use ($model, $pivotData) {\n                    $this->attach($model, $pivotData);\n                });\n            }\n\n            $this->parent->unsetRelation($this->relationName);\n        }\n        else {\n            $this->parent->bindDeferred($this->relationName, $model, $sessionKey, $pivotData);\n        }\n    }\n\n    /**\n     * remove a model from this relationship type.\n     */\n    public function remove(Model $model, $sessionKey = null)\n    {\n        if ($sessionKey === null) {\n            $this->detach($model);\n            $this->parent->unsetRelation($this->relationName);\n        }\n        else {\n            $this->parent->unbindDeferred($this->relationName, $model, $sessionKey);\n        }\n    }\n\n    /**\n     * paginate gets a paginator for the \"select\" statement that complies with October Rain\n     *\n     * @param  int    $perPage\n     * @param  int    $currentPage\n     * @param  array  $columns\n     * @param  string  $pageName\n     * @return \\Illuminate\\Contracts\\Pagination\\LengthAwarePaginator\n     */\n    public function paginate($perPage = null, $columns = ['*'], $pageName = 'page', $currentPage = null)\n    {\n        // Legacy signature support\n        // paginate($perPage, $currentPage, $columns, $pageName)\n        if (!is_array($columns)) {\n            $_currentPage = $columns;\n            $_columns = $pageName;\n            $_pageName = $currentPage;\n\n            $columns = is_array($_columns) ? $_columns : ['*'];\n            $pageName = $_pageName !== null ? $_pageName : 'page';\n            $currentPage = is_array($_currentPage) ? null : $_currentPage;\n        }\n\n        $this->query->addSelect($this->shouldSelect($columns));\n\n        $paginator = $this->query->paginate($perPage, $currentPage, $columns);\n\n        $this->hydratePivotRelation($paginator->items());\n\n        return $paginator;\n    }\n\n    /**\n     * simplePaginate using a simple paginator.\n     *\n     * @param  int|null  $perPage\n     * @param  array  $columns\n     * @param  string  $pageName\n     * @param  int|null  $page\n     * @return \\Illuminate\\Contracts\\Pagination\\Paginator\n     */\n    public function simplePaginate($perPage = null, $columns = ['*'], $pageName = 'page', $currentPage = null)\n    {\n        // Legacy signature support\n        // paginate($perPage, $currentPage, $columns, $pageName)\n        if (!is_array($columns)) {\n            $_currentPage = $columns;\n            $_columns = $pageName;\n            $_pageName = $currentPage;\n\n            $columns = is_array($_columns) ? $_columns : ['*'];\n            $pageName = $_pageName !== null ? $_pageName : 'page';\n            $currentPage = is_array($_currentPage) ? null : $_currentPage;\n        }\n\n        $this->query->addSelect($this->shouldSelect($columns));\n\n        $paginator = $this->query->simplePaginate($perPage, $currentPage, $columns);\n\n        $this->hydratePivotRelation($paginator->items());\n\n        return $paginator;\n    }\n\n    /**\n     * cursorPaginate using a cursor paginator.\n     *\n     * @param  int|null  $perPage\n     * @param  array  $columns\n     * @param  string  $cursorName\n     * @param  \\Illuminate\\Pagination\\Cursor|string|null  $cursor\n     * @return \\Illuminate\\Contracts\\Pagination\\CursorPaginator\n     */\n    public function cursorPaginate($perPage = null, $columns = ['*'], $cursorName = 'cursor', $cursor = null)\n    {\n        $this->query->addSelect($this->shouldSelect($columns));\n\n        $paginator = $this->query->cursorPaginate($perPage, $columns, $cursorName, $cursor);\n\n        $this->hydratePivotRelation($paginator->items());\n\n        return $paginator;\n    }\n\n    /**\n     * newPivot creates a new pivot model instance\n     *\n     * @param  array  $attributes\n     * @param  bool   $exists\n     * @return \\Illuminate\\Database\\Eloquent\\Relations\\Pivot\n     */\n    public function newPivot(array $attributes = [], $exists = false)\n    {\n        // October looks to the relationship parent\n        $pivot = $this->parent->newRelationPivot($this->relationName, $this->parent, $attributes, $this->table, $exists);\n\n        // Laravel looks to the related model\n        if (empty($pivot)) {\n            $pivot = $this->related->newPivot($this->parent, $attributes, $this->table, $exists, $this->using);\n        }\n\n        return $pivot->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey);\n    }\n\n    /**\n     * setSimpleValue helper for setting this relationship using various expected\n     * values. For example, $model->relation = $value;\n     */\n    public function setSimpleValue($value)\n    {\n        // Nulling the relationship\n        if (!$value) {\n            // Disassociate in memory immediately\n            $this->parent->setRelation(\n                $this->relationName,\n                $this->getRelated()->newCollection()\n            );\n\n            // Perform sync when the model is saved\n            $this->parent->bindEventOnce('model.afterSave', function () use ($value) {\n                $this->detach();\n            });\n            return;\n        }\n\n        // Convert models to keys\n        if ($value instanceof Model) {\n            $value = $value->{$this->getRelatedKeyName()};\n        }\n        elseif (is_array($value)) {\n            foreach ($value as $_key => $_value) {\n                if ($_value instanceof Model) {\n                    $value[$_key] = $_value->{$this->getRelatedKeyName()};\n                }\n            }\n        }\n\n        // Setting the relationship\n        $relationCollection = $value instanceof CollectionBase\n            ? $value\n            : $this->newSimpleRelationQuery((array) $value)->get();\n\n        // Associate in memory immediately\n        $this->parent->setRelation($this->relationName, $relationCollection);\n\n        // Perform sync when the model is saved\n        $this->parent->bindEventOnce('model.afterSave', function () use ($value) {\n            $this->sync($value);\n        });\n    }\n\n    /**\n     * newSimpleRelationQuery for the related instance based on an array of IDs.\n     */\n    protected function newSimpleRelationQuery(array $ids)\n    {\n        $model = $this->getRelated();\n\n        $query = $model->newQuery();\n\n        return $query->whereIn($this->getRelatedKeyName(), $ids);\n    }\n\n    /**\n     * getSimpleValue is a helper for getting this relationship simple value,\n     * generally useful with form values\n     */\n    public function getSimpleValue()\n    {\n        $value = [];\n        $relationName = $this->relationName;\n\n        if ($this->parent->relationLoaded($relationName)) {\n            $value = $this->parent->getRelation($relationName)\n                ->pluck($this->getRelatedKeyName())\n                ->all()\n            ;\n        }\n        else {\n            $value = $this->allRelatedIds()->all();\n        }\n\n        return $value;\n    }\n\n    /**\n     * @deprecated use getQualifiedForeignPivotKeyName\n     */\n    public function getForeignKey()\n    {\n        return $this->table.'.'.$this->foreignPivotKey;\n    }\n\n    /**\n     * @deprecated use getQualifiedRelatedPivotKeyName\n     */\n    public function getOtherKey()\n    {\n        return $this->table.'.'.$this->relatedPivotKey;\n    }\n\n    /**\n     * shouldSelect gets the select columns for the relation query\n     * @return \\Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany\n     */\n    protected function shouldSelect(array $columns = ['*'])\n    {\n        // @deprecated remove this whole method when `countMode` is gone\n        if ($this->countMode) {\n            return $this->table.'.'.$this->foreignPivotKey.' as pivot_'.$this->foreignPivotKey;\n        }\n\n        if ($columns === ['*']) {\n            $columns = [$this->related->getTable().'.*'];\n        }\n\n        return array_merge($columns, $this->aliasedPivotColumns());\n    }\n\n    /**\n     * performJoin will join the pivot table opportunistically instead of mandatorily\n     * to support deferred bindings that exist in another table.\n     *\n     * This method is based on `performJoin` method logic except it uses a left join.\n     *\n     * @param  \\Illuminate\\Database\\Eloquent\\Builder|null  $query\n     * @return $this\n     */\n    protected function performLeftJoin($query = null)\n    {\n        $query = $query ?: $this->query;\n\n        $query->leftJoin($this->table, function($join) {\n            $join->on($this->getQualifiedRelatedKeyName(), '=', $this->getQualifiedRelatedPivotKeyName());\n            $join->where($this->getQualifiedForeignPivotKeyName(), $this->parent->getKey());\n        });\n\n        return $this;\n    }\n\n    /**\n     * performSortableColumnJoin includes custom logic to replace the sort order column with\n     * a unified column\n     */\n    protected function performSortableColumnJoin($query = null, $sessionKey = null)\n    {\n        if (\n            !$this->parent->isClassInstanceOf(\\October\\Contracts\\Database\\SortableRelationInterface::class) ||\n            !$this->parent->isSortableRelation($this->relationName)\n        ) {\n            return;\n        }\n\n        // Check if sorting by the matched sort_order column\n        $sortColumn = $this->qualifyPivotColumn(\n            $this->parent->getRelationSortOrderColumn($this->relationName)\n        );\n\n        $orderDefinitions = $query->getQuery()->orders;\n\n        if (!is_array($orderDefinitions)) {\n            return;\n        }\n\n        $sortableIndex = false;\n        foreach ($orderDefinitions as $index => $order) {\n            if ($order['column'] === $sortColumn) {\n                $sortableIndex = $index;\n            }\n        }\n\n        // Not sorting by the sort column, abort\n        if ($sortableIndex === false) {\n            return;\n        }\n\n        // Join the deferred binding table and select the combo column\n        $tempOrderColumns = 'october_reserved_sort_order';\n        $combinedOrderColumn = \"ifnull(deferred_bindings.sort_order, {$sortColumn}) as {$tempOrderColumns}\";\n        $this->performDeferredLeftJoin($query, $sessionKey);\n        $this->addSelect(DbDongle::raw($combinedOrderColumn));\n\n        // Overwrite the sortable column with the combined one\n        $query->getQuery()->orders[$sortableIndex]['column'] = $tempOrderColumns;\n    }\n\n    /**\n     * performDeferredLeftJoin left joins the deferred bindings table\n     */\n    protected function performDeferredLeftJoin($query = null, $sessionKey = null)\n    {\n        $query = $query ?: $this->query;\n\n        $query->leftJoin('deferred_bindings', function($join) use ($sessionKey) {\n            $join->on(\n                $this->getQualifiedRelatedKeyName(), '=', 'deferred_bindings.slave_id')\n                    ->where('master_field', $this->relationName)\n                    ->where('master_type', get_class($this->parent))\n                    ->where('session_key', $sessionKey);\n        });\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Database/Relations/DeferOneOrMany.php",
    "content": "<?php namespace October\\Rain\\Database\\Relations;\n\nuse October\\Rain\\Support\\Facades\\DbDongle;\nuse October\\Rain\\Database\\Relations\\BelongsToMany as BelongsToManyBase;\n\n/**\n * DeferOneOrMany\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait DeferOneOrMany\n{\n    /**\n     * withDeferred returns a new model query with deferred bindings added, this\n     * will reset any constraints that come before it\n     * @param string|null $sessionKey\n     * @return \\Illuminate\\Database\\Query\\Builder\n     */\n    public function withDeferred($sessionKey = null)\n    {\n        $newQuery = $this->query->getQuery()->newQuery();\n        $newQuery->from($this->related->getTable());\n\n        // Readd the defined constraints\n        $this->addDefinedConstraintsToQuery($newQuery);\n\n        // Apply deferred binding to the new query\n        $newQuery = $this->withDeferredQuery($newQuery, $sessionKey);\n\n        // Bless this query with the deferred query\n        $this->query->setQuery($newQuery);\n\n        // Readd the global scopes\n        foreach ($this->related->getGlobalScopes() as $identifier => $scope) {\n            $this->query->withGlobalScope($identifier, $scope);\n        }\n\n        return $this->query;\n    }\n\n    /**\n     * withDeferredQuery returns the supplied model query, or current model query, with\n     * deferred bindings added, this will preserve any constraints that came before it\n     * @param \\Illuminate\\Database\\Query\\Builder|null $newQuery\n     * @param string|null $sessionKey\n     * @return \\Illuminate\\Database\\Query\\Builder\n     */\n    public function withDeferredQuery($newQuery = null, $sessionKey = null)\n    {\n        if ($newQuery === null) {\n            $newQuery = $this->query->getQuery();\n        }\n\n        // Guess the key from the parent model\n        if ($sessionKey === null) {\n            $sessionKey = $this->parent->sessionKey;\n        }\n\n        // Swap the standard inner join for a left join\n        if ($this instanceof BelongsToManyBase) {\n            $this->performLeftJoin($newQuery);\n            $this->performSortableColumnJoin($newQuery, $sessionKey);\n        }\n\n        $newQuery->where(function ($query) use ($sessionKey) {\n            // Trick the relation to add constraints to this nested query\n            if ($this->parent->exists) {\n                $oldQuery = $this->query;\n                $this->query->setQuery($query);\n                $this->addConstraints();\n                $this->query->setQuery($oldQuery);\n            }\n\n            // Bind (Add)\n            $query = $query->orWhereIn($this->related->getQualifiedKeyName(), function ($query) use ($sessionKey) {\n                $query\n                    ->select('slave_id')\n                    ->from('deferred_bindings')\n                    ->where('master_field', $this->relationName)\n                    ->where('master_type', get_class($this->parent))\n                    ->where('session_key', $sessionKey)\n                    ->where('is_bind', 1);\n            });\n        });\n\n        // Unbind (Remove)\n        $newQuery->whereNotIn($this->related->getQualifiedKeyName(), function ($query) use ($sessionKey) {\n            $query\n                ->select('slave_id')\n                ->from('deferred_bindings')\n                ->where('master_field', $this->relationName)\n                ->where('master_type', get_class($this->parent))\n                ->where('session_key', $sessionKey)\n                ->where('is_bind', 0)\n                ->whereRaw(DbDongle::parse('id > ifnull((select max(id) from '.DbDongle::getTablePrefix().'deferred_bindings where\n                        slave_id = '.$this->getWithDeferredQualifiedKeyName().' and\n                        master_field = ? and\n                        master_type = ? and\n                        session_key = ? and\n                        is_bind = ?\n                    ), 0)'), [\n                    $this->relationName,\n                    get_class($this->parent),\n                    $sessionKey,\n                    1\n                ]);\n        });\n\n        return $newQuery;\n    }\n\n    /**\n     * getWithDeferredQualifiedKeyName returns the related \"slave id\" key\n     * in a database friendly format.\n     * @return \\Illuminate\\Database\\Query\\Expression\n     */\n    protected function getWithDeferredQualifiedKeyName()\n    {\n        return DbDongle::rawValue(\n            DbDongle::getTablePrefix() . $this->related->getQualifiedKeyName()\n        );\n    }\n}\n"
  },
  {
    "path": "src/Database/Relations/DefinedConstraints.php",
    "content": "<?php namespace October\\Rain\\Database\\Relations;\n\nuse Site;\nuse Illuminate\\Database\\Eloquent\\Relations\\BelongsToMany as BelongsToManyBase;\nuse Illuminate\\Support\\Arr;\n\n/**\n * DefinedConstraints handles the constraints and filters defined by a relation\n * eg: 'conditions' => 'is_published = 1'\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait DefinedConstraints\n{\n    /**\n     * @var bool pivotSiteScope indicates if pivot queries should be scoped by site_id.\n     * This is used for dual-multisite scenarios where both parent and related models use multisite.\n     */\n    protected $pivotSiteScope = false;\n\n    /**\n     * addDefinedConstraints to the relation query\n     */\n    public function addDefinedConstraints(): void\n    {\n        $args = $this->getRelationDefinitionForDefinedConstraints();\n\n        $this->addDefinedConstraintsToRelation($this, $args);\n\n        $this->addDefinedConstraintsToQuery($this, $args);\n    }\n\n    /**\n     * addDefinedConstraintsToRelation\n     */\n    public function addDefinedConstraintsToRelation($relation, ?array $args = null)\n    {\n        if ($args === null) {\n            $args = $this->getRelationDefinitionForDefinedConstraints();\n        }\n\n        // Default models (belongsTo, hasOne, hasOneThrough, morphOne)\n        if ($defaultData = Arr::get($args, 'default')) {\n            $relation->withDefault($defaultData);\n        }\n\n        // Pivot data (belongsToMany, morphToMany, morphByMany)\n        if ($pivotData = Arr::get($args, 'pivot')) {\n            $relation->withPivot($pivotData);\n        }\n\n        // Pivot incrementing key (belongsToMany, morphToMany, morphByMany)\n        if ($pivotKey = Arr::get($args, 'pivotKey')) {\n            $relation->withPivot($pivotKey);\n        }\n\n        // Pivot timestamps (belongsToMany, morphToMany, morphByMany)\n        if (Arr::get($args, 'timestamps')) {\n            $relation->withTimestamps();\n        }\n\n        // Count \"helper\" relation\n        // @deprecated use Laravel withCount() method instead\n        if (Arr::get($args, 'count')) {\n            if ($relation instanceof BelongsToManyBase) {\n                $relation->countMode = true;\n                $keyName = $relation->getQualifiedForeignPivotKeyName();\n            }\n            else {\n                $keyName = $relation->getForeignKeyName();\n            }\n\n            $countSql = $this->parent->getConnection()->raw('count(*) as count');\n\n            $relation->select($keyName, $countSql)->groupBy($keyName)->orderBy($keyName);\n        }\n\n        // Pivot site scope (dual-multisite: both parent and related models use multisite)\n        // This enables site_id scoping on pivot table queries\n        if (Arr::get($args, 'pivotSiteScope')) {\n            $this->pivotSiteScope = true;\n            $relation->withPivot(['site_id']);\n        }\n    }\n\n    /**\n     * isPivotSiteScoped returns true if pivot queries should be scoped by site_id.\n     */\n    public function isPivotSiteScoped(): bool\n    {\n        return $this->pivotSiteScope;\n    }\n\n    /**\n     * addPivotSiteScopeConstraints adds site_id constraint to pivot queries if enabled.\n     */\n    protected function addPivotSiteScopeConstraints(): void\n    {\n        if ($this->pivotSiteScope && !Site::hasGlobalContext()) {\n            $this->where($this->qualifyPivotColumn('site_id'), Site::getSiteIdFromContext());\n        }\n    }\n\n    /**\n     * getPivotSiteScopeValue returns the current site_id value for pivot records.\n     * Returns null if pivotSiteScope is disabled, otherwise returns the site ID.\n     */\n    protected function getPivotSiteScopeValue(): ?int\n    {\n        if (!$this->pivotSiteScope) {\n            return null;\n        }\n\n        $siteId = Site::getSiteIdFromContext();\n\n        // In dual-multisite, we always need a site_id for pivot records\n        // If there's no site context, it likely means we're in global context\n        // during propagation - the sync should happen inside Site::withContext()\n        if ($siteId === null) {\n            return null;\n        }\n\n        return $siteId;\n    }\n\n    /**\n     * addDefinedConstraintsToQuery\n     */\n    public function addDefinedConstraintsToQuery($query, ?array $args = null)\n    {\n        if ($args === null) {\n            $args = $this->getRelationDefinitionForDefinedConstraints();\n        }\n\n        // Conditions\n        if ($conditions = Arr::get($args, 'conditions')) {\n            $query->whereRaw($conditions);\n        }\n\n        // Sort order\n        // @deprecated count is deprecated\n        $hasCountArg = Arr::get($args, 'count') !== null;\n        if (($orderBy = Arr::get($args, 'order')) && !$hasCountArg) {\n            if (!is_array($orderBy)) {\n                $orderBy = [$orderBy];\n            }\n\n            foreach ($orderBy as $order) {\n                $column = $order;\n                $direction = 'asc';\n\n                $parts = explode(' ', $order);\n                if (count($parts) > 1) {\n                    [$column, $direction] = $parts;\n                }\n\n                $query->orderBy($column, $direction);\n            }\n        }\n\n        // Scope\n        if ($scope = Arr::get($args, 'scope')) {\n            if (is_string($scope)) {\n                $query->$scope($this->parent);\n            }\n            else {\n                $scope($query, $this->parent, $this->related);\n            }\n        }\n    }\n\n    /**\n     * getRelationDefinitionForDefinedConstraints returns the relation definition for the\n     * relationship context.\n     */\n    protected function getRelationDefinitionForDefinedConstraints()\n    {\n        return $this->parent->getRelationDefinition($this->relationName);\n    }\n}\n"
  },
  {
    "path": "src/Database/Relations/HasMany.php",
    "content": "<?php namespace October\\Rain\\Database\\Relations;\n\nuse October\\Rain\\Database\\Collection;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Collection as CollectionBase;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasMany as HasManyBase;\n\n/**\n * HasMany\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass HasMany extends HasManyBase\n{\n    use HasOneOrMany;\n    use DefinedConstraints;\n\n    /**\n     * __construct a new has many relationship instance.\n     */\n    public function __construct(Builder $query, Model $parent, $foreignKey, $localKey, $relationName = null)\n    {\n        $this->relationName = $relationName;\n\n        parent::__construct($query, $parent, $foreignKey, $localKey);\n\n        $this->addDefinedConstraints();\n    }\n\n    /**\n     * setSimpleValue helper for setting this relationship using various expected\n     * values. For example, $model->relation = $value;\n     */\n    public function setSimpleValue($value)\n    {\n        // Nulling the relationship\n        if (!$value) {\n            $this->parent->unsetRelation($this->relationName);\n\n            if ($this->parent->exists) {\n                $this->parent->bindEventOnce('model.afterSave', function() {\n                    $this->ensureRelationIsEmpty();\n                });\n            }\n            return;\n        }\n\n        if ($value instanceof Model) {\n            $value = new Collection([$value]);\n        }\n\n        if ($value instanceof CollectionBase) {\n            $collection = $value;\n\n            if ($this->parent->exists) {\n                $collection->each(function($instance) {\n                    $instance->setAttribute($this->getForeignKeyName(), $this->getParentKey());\n                });\n            }\n        }\n        else {\n            $collection = $this->getRelated()\n                ->whereIn($this->getRelatedKeyName(), (array) $value)\n                ->get()\n            ;\n        }\n\n        if (!$collection) {\n            return;\n        }\n\n        $this->parent->setRelation($this->relationName, $collection);\n\n        $this->parent->bindEventOnce('model.afterSave', function() use ($collection) {\n            $existingIds = $collection->pluck($this->getRelatedKeyName())->all();\n\n            $this->whereNotIn($this->getRelatedKeyName(), $existingIds)->update([\n                $this->getForeignKeyName() => null\n            ]);\n\n            $collection->each(function($instance) {\n                $instance->setAttribute($this->getForeignKeyName(), $this->getParentKey());\n                $instance->save(['timestamps' => false]);\n            });\n        });\n    }\n\n    /**\n     * getSimpleValue helper for getting this relationship simple value,\n     * generally useful with form values.\n     */\n    public function getSimpleValue()\n    {\n        $value = null;\n        $relationName = $this->relationName;\n\n        if ($this->parent->relationLoaded($relationName)) {\n            $value = $this->parent->getRelation($relationName)\n                ->pluck($this->getRelatedKeyName())\n                ->all()\n            ;\n        }\n        else {\n            $value = $this->query->getQuery()\n                ->pluck($this->getRelatedKeyName())\n                ->all()\n            ;\n        }\n\n        return $value;\n    }\n}\n"
  },
  {
    "path": "src/Database/Relations/HasManyThrough.php",
    "content": "<?php namespace October\\Rain\\Database\\Relations;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasManyThrough as HasManyThroughBase;\n\n/**\n * HasManyThrough class extension\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass HasManyThrough extends HasManyThroughBase\n{\n    use DefinedConstraints;\n\n    /**\n     * @var string relationName\n     */\n    protected $relationName;\n\n    /**\n     * __construct a new has many relationship instance.\n     */\n    public function __construct(Builder $query, Model $farParent, Model $parent, $firstKey, $secondKey, $localKey, $secondLocalKey, $relationName = null)\n    {\n        $this->relationName = $relationName;\n\n        parent::__construct($query, $farParent, $parent, $firstKey, $secondKey, $localKey, $secondLocalKey);\n\n        $this->addDefinedConstraints();\n    }\n\n    /**\n     * getRelationDefinitionForDefinedConstraints returns the relation definition for the\n     * relationship context.\n     */\n    protected function getRelationDefinitionForDefinedConstraints()\n    {\n        return $this->farParent->getRelationDefinition($this->relationName);\n    }\n\n    /**\n     * parentSoftDeletes determines whether close parent of the relation uses Soft Deletes.\n     * @return bool\n     */\n    public function parentSoftDeletes()\n    {\n        $uses = class_uses_recursive(get_class($this->parent));\n\n        return in_array(\\October\\Rain\\Database\\Traits\\SoftDelete::class, $uses) ||\n            in_array(\\Illuminate\\Database\\Eloquent\\SoftDeletes::class, $uses);\n    }\n\n    /**\n     * getSimpleValue is a helper for getting this relationship simple value,\n     * generally useful with form values.\n     */\n    public function getSimpleValue()\n    {\n        $value = null;\n        $relationName = $this->relationName;\n\n        if ($this->farParent->relationLoaded($relationName)) {\n            $value = $this->farParent->getRelation($relationName)\n                ->pluck($this->getRelatedKeyName())\n                ->all()\n            ;\n        }\n        else {\n            $value = $this->query->getQuery()\n                ->pluck($this->getQualifiedRelatedKeyName())\n                ->all()\n            ;\n        }\n\n        return $value;\n    }\n\n    /**\n     * getRelatedKeyName\n     * @return string\n     */\n    public function getRelatedKeyName()\n    {\n        return $this->related->getKeyName();\n    }\n\n    /**\n     * getQualifiedRelatedKeyName\n     * @return string\n     */\n    public function getQualifiedRelatedKeyName()\n    {\n        return $this->related->getQualifiedKeyName();\n    }\n}\n"
  },
  {
    "path": "src/Database/Relations/HasOne.php",
    "content": "<?php namespace October\\Rain\\Database\\Relations;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOne as HasOneBase;\n\n/**\n * HasOne\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass HasOne extends HasOneBase\n{\n    use HasOneOrMany;\n    use DefinedConstraints;\n\n    /**\n     * __construct a new has many relationship instance.\n     */\n    public function __construct(Builder $query, Model $parent, $foreignKey, $localKey, $relationName = null)\n    {\n        $this->relationName = $relationName;\n\n        parent::__construct($query, $parent, $foreignKey, $localKey);\n\n        $this->addDefinedConstraints();\n    }\n\n    /**\n     * setSimpleValue helper for setting this relationship using various expected\n     * values. For example, $model->relation = $value;\n     */\n    public function setSimpleValue($value)\n    {\n        if (is_array($value)) {\n            $value = current($value);\n        }\n\n        // Nulling the relationship\n        if (!$value) {\n            $this->parent->setRelation($this->relationName, null);\n\n            if ($this->parent->exists) {\n                $this->parent->bindEventOnce('model.afterSave', function() {\n                    $this->ensureRelationIsEmpty();\n                });\n            }\n            return;\n        }\n\n        if ($value instanceof Model) {\n            $instance = $value;\n\n            if ($this->parent->exists) {\n                $instance->setAttribute($this->getForeignKeyName(), $this->getParentKey());\n            }\n        }\n        else {\n            $instance = $this->getRelated()->find($value);\n        }\n\n        if (!$instance) {\n            return;\n        }\n\n        $this->parent->setRelation($this->relationName, $instance);\n\n        $this->parent->bindEventOnce('model.afterSave', function() use ($instance) {\n            // Relation is already set, do nothing. This prevents the relationship\n            // from being nulled below and left unset because the save will ignore\n            // attribute values that are numerically equivalent (not dirty).\n            if ($instance->getOriginal($this->getForeignKeyName()) == $this->getParentKey()) {\n                return;\n            }\n\n            $this->update([$this->getForeignKeyName() => null]);\n            $instance->setAttribute($this->getForeignKeyName(), $this->getParentKey());\n            $instance->save(['timestamps' => false]);\n        });\n    }\n\n    /**\n     * getSimpleValue helper for getting this relationship simple value,\n     * generally useful with form values.\n     */\n    public function getSimpleValue()\n    {\n        $value = null;\n        $relationName = $this->relationName;\n\n        if ($related = $this->parent->{$relationName}) {\n            $key = $this->getRelatedKeyName();\n            $value = $related->{$key};\n        }\n\n        return $value;\n    }\n}\n"
  },
  {
    "path": "src/Database/Relations/HasOneOrMany.php",
    "content": "<?php namespace October\\Rain\\Database\\Relations;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Support\\Arr;\n\n/**\n * HasOneOrMany\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasOneOrMany\n{\n    use DeferOneOrMany;\n\n    /**\n     * @var string relationName is the \"name\" of the relationship.\n     */\n    protected $relationName;\n\n    /**\n     * save the supplied related model with deferred binding support.\n     */\n    public function save(Model $model, $sessionKey = null)\n    {\n        if ($sessionKey === null) {\n            return parent::save($model);\n        }\n\n        $this->add($model, $sessionKey);\n        return $model->save() ? $model : false;\n    }\n\n    /**\n     * saveQuietly saves the supplied related model without raising any events,\n     * with deferred binding support.\n     */\n    public function saveQuietly(Model $model, $sessionKey = null)\n    {\n        return Model::withoutEvents(function () use ($model, $sessionKey) {\n            return $this->save($model, $sessionKey);\n        });\n    }\n\n    /**\n     * saveMany is an alias for the addMany() method\n     * @param  array  $models\n     * @return array\n     */\n    public function saveMany($models, $sessionKey = null)\n    {\n        $this->addMany($models, $sessionKey);\n\n        return $models;\n    }\n\n    /**\n     * saveManyQuietly saves multiple models without raising any events,\n     * with deferred binding support.\n     */\n    public function saveManyQuietly($models, $sessionKey = null)\n    {\n        return Model::withoutEvents(function () use ($models, $sessionKey) {\n            return $this->saveMany($models, $sessionKey);\n        });\n    }\n\n    /**\n     * create a new instance of this related model with deferred binding support\n     */\n    public function create(array $attributes = [], $sessionKey = null)\n    {\n        $model = parent::create($attributes);\n\n        if ($sessionKey !== null) {\n            $this->add($model, $sessionKey);\n        }\n\n        return $model;\n    }\n\n    /**\n     * createQuietly creates a new instance without raising any events,\n     * with deferred binding support.\n     */\n    public function createQuietly(array $attributes = [], $sessionKey = null)\n    {\n        return Model::withoutEvents(function () use ($attributes, $sessionKey) {\n            return $this->create($attributes, $sessionKey);\n        });\n    }\n\n    /**\n     * forceCreateQuietly creates a new instance bypassing mass assignment\n     * without raising any events, with deferred binding support.\n     */\n    public function forceCreateQuietly(array $attributes = [], $sessionKey = null)\n    {\n        return Model::withoutEvents(function () use ($attributes, $sessionKey) {\n            $model = parent::forceCreate($attributes);\n\n            if ($sessionKey !== null) {\n                $this->add($model, $sessionKey);\n            }\n\n            return $model;\n        });\n    }\n\n    /**\n     * createMany creates multiple related models with deferred binding support.\n     */\n    public function createMany(iterable $records, $sessionKey = null)\n    {\n        $instances = parent::createMany($records);\n\n        if ($sessionKey !== null) {\n            foreach ($instances as $model) {\n                $this->add($model, $sessionKey);\n            }\n        }\n\n        return $instances;\n    }\n\n    /**\n     * createManyQuietly creates multiple models without raising any events,\n     * with deferred binding support.\n     */\n    public function createManyQuietly(iterable $records, $sessionKey = null)\n    {\n        return Model::withoutEvents(function () use ($records, $sessionKey) {\n            return $this->createMany($records, $sessionKey);\n        });\n    }\n\n    /**\n     * createOrFirst attempts to create the record, or if a unique constraint\n     * violation occurs, finds the existing record.\n     */\n    public function createOrFirst(array $attributes = [], \\Closure|array $values = [], $sessionKey = null)\n    {\n        $model = parent::createOrFirst($attributes, $values);\n\n        if ($sessionKey !== null) {\n            $this->add($model, $sessionKey);\n        }\n\n        return $model;\n    }\n\n    /**\n     * add a model to this relationship type\n     */\n    public function add(Model $model, $sessionKey = null)\n    {\n        if ($sessionKey === null) {\n            /**\n             * @event model.relation.beforeAdd\n             * Called before adding a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)\n             *\n             * Example usage:\n             *\n             *     $model->bindEvent('model.relation.beforeAdd', function (string $relationName, \\October\\Rain\\Database\\Model $relatedModel) use (\\October\\Rain\\Database\\Model $model) {\n             *         if ($relationName === 'some_relation') {\n             *             return false;\n             *         }\n             *     });\n             *\n             */\n            if ($this->parent->fireEvent('model.relation.beforeAdd', [$this->relationName, $model], true) === false) {\n                return;\n            }\n\n            // Associate the model\n            if ($this->parent->exists) {\n                $model->setAttribute($this->getForeignKeyName(), $this->getParentKey());\n                $model->save();\n            }\n            else {\n                $this->parent->bindEventOnce('model.afterSave', function () use ($model) {\n                    $model->setAttribute($this->getForeignKeyName(), $this->getParentKey());\n                    $model->save();\n                });\n            }\n\n            // Use the opportunity to set the relation in memory\n            if ($this instanceof HasOne) {\n                $this->parent->setRelation($this->relationName, $model);\n            }\n            else {\n                $this->parent->unsetRelation($this->relationName);\n            }\n\n            /**\n             * @event model.relation.add\n             * Called after adding a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)\n             *\n             * Example usage:\n             *\n             *     $model->bindEvent('model.relation.add', function (string $relationName, \\October\\Rain\\Database\\Model $relatedModel) use (\\October\\Rain\\Database\\Model $model) {\n             *         $relatedClass = get_class($relatedModel);\n             *         $modelClass = get_class($model);\n             *         traceLog(\"{$relatedClass} was added as {$relationName} to {$modelClass}.\");\n             *     });\n             *\n             */\n            $this->parent->fireEvent('model.relation.add', [$this->relationName, $model]);\n        }\n        else {\n            $this->parent->bindDeferred($this->relationName, $model, $sessionKey);\n        }\n    }\n\n    /**\n     * addMany attaches an array of models to the parent instance with deferred binding support\n     * @param  array  $models\n     * @return void\n     */\n    public function addMany($models, $sessionKey = null)\n    {\n        foreach ($models as $model) {\n            $this->add($model, $sessionKey);\n        }\n    }\n\n    /**\n     * remove a model from this relationship type.\n     */\n    public function remove(Model $model, $sessionKey = null)\n    {\n        if ($sessionKey === null) {\n            /**\n             * @event model.relation.beforeRemove\n             * Called before removing a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)\n             *\n             * Example usage:\n             *\n             *     $model->bindEvent('model.relation.beforeRemove', function (string $relationName, \\October\\Rain\\Database\\Model $relatedModel) use (\\October\\Rain\\Database\\Model $model) {\n             *         if ($relationName === 'perm_relation') {\n             *             return false;\n             *         }\n             *     });\n             *\n             */\n            if ($this->parent->fireEvent('model.relation.beforeRemove', [$this->relationName, $model], true) === false) {\n                return;\n            }\n\n            if (!$this->isModelRemovable($model)) {\n                return;\n            }\n\n            $options = $this->parent->getRelationDefinition($this->relationName);\n\n            // Delete or orphan the model\n            if (Arr::get($options, 'delete', false)) {\n                $model->delete();\n            }\n            else {\n                $model->setAttribute($this->getForeignKeyName(), null);\n                $model->save();\n            }\n\n            // Use this opportunity to set the relation in memory\n            if ($this instanceof HasOne) {\n                $this->parent->setRelation($this->relationName, null);\n            }\n            else {\n                $this->parent->unsetRelation($this->relationName);\n            }\n\n            /**\n             * @event model.relation.remove\n             * Called after removing a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)\n             *\n             * Example usage:\n             *\n             *     $model->bindEvent('model.relation.remove', function (string $relationName, \\October\\Rain\\Database\\Model $relatedModel) use (\\October\\Rain\\Database\\Model $model) {\n             *         $relatedClass = get_class($relatedModel);\n             *         $modelClass = get_class($model);\n             *         traceLog(\"{$relatedClass} was removed from {$modelClass}.\");\n             *     });\n             *\n             */\n            $this->parent->fireEvent('model.relation.remove', [$this->relationName, $model]);\n        }\n        else {\n            $this->parent->unbindDeferred($this->relationName, $model, $sessionKey);\n        }\n    }\n\n    /**\n     * isModelRemovable returns true if an existing model is already associated\n     */\n    protected function isModelRemovable($model): bool\n    {\n        return ((string) $model->getAttribute($this->getForeignKeyName()) === (string) $this->getParentKey());\n    }\n\n    /**\n     * ensureRelationIsEmpty ensures the relation is empty, either deleted or nulled.\n     */\n    protected function ensureRelationIsEmpty()\n    {\n        $options = $this->parent->getRelationDefinition($this->relationName);\n\n        if (Arr::get($options, 'delete', false)) {\n            $this->delete();\n        }\n        else {\n            $this->update([$this->getForeignKeyName() => null]);\n        }\n    }\n\n    /**\n     * getRelatedKeyName\n     * @return string\n     */\n    public function getRelatedKeyName()\n    {\n        return $this->related->getKeyName();\n    }\n\n    /**\n     * @deprecated use getForeignKeyName\n     */\n    public function getForeignKey()\n    {\n        return $this->foreignKey;\n    }\n\n    /**\n     * @deprecated use getLocalKeyName\n     */\n    public function getOtherKey()\n    {\n        return $this->localKey;\n    }\n}\n"
  },
  {
    "path": "src/Database/Relations/HasOneThrough.php",
    "content": "<?php namespace October\\Rain\\Database\\Relations;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOneThrough as HasOneThroughBase;\n\n/**\n * HasOneThrough\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass HasOneThrough extends HasOneThroughBase\n{\n    use DefinedConstraints;\n\n    /**\n     * @var string The \"name\" of the relationship.\n     */\n    protected $relationName;\n\n    /**\n     * __construct a new has many relationship instance.\n     */\n    public function __construct(Builder $query, Model $farParent, Model $parent, $firstKey, $secondKey, $localKey, $secondLocalKey, $relationName = null)\n    {\n        $this->relationName = $relationName;\n\n        parent::__construct($query, $farParent, $parent, $firstKey, $secondKey, $localKey, $secondLocalKey);\n\n        $this->addDefinedConstraints();\n    }\n\n    /**\n     * parentSoftDeletes determines whether close parent of the relation uses Soft Deletes.\n     * @return bool\n     */\n    public function parentSoftDeletes()\n    {\n        $uses = class_uses_recursive(get_class($this->parent));\n\n        return in_array(\\October\\Rain\\Database\\Traits\\SoftDelete::class, $uses) ||\n            in_array(\\Illuminate\\Database\\Eloquent\\SoftDeletes::class, $uses);\n    }\n}\n"
  },
  {
    "path": "src/Database/Relations/MorphMany.php",
    "content": "<?php namespace October\\Rain\\Database\\Relations;\n\nuse October\\Rain\\Database\\Collection;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Collection as CollectionBase;\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphMany as MorphManyBase;\n\n/**\n * MorphMany\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass MorphMany extends MorphManyBase\n{\n    use MorphOneOrMany;\n    use DefinedConstraints;\n\n    /**\n     * __construct a new has many relationship instance.\n     */\n    public function __construct(Builder $query, Model $parent, $type, $id, $localKey, $relationName = null)\n    {\n        $this->relationName = $relationName;\n\n        parent::__construct($query, $parent, $type, $id, $localKey);\n\n        $this->addDefinedConstraints();\n    }\n\n    /**\n     * setSimpleValue helper for setting this relationship using various expected\n     * values. For example, $model->relation = $value;\n     */\n    public function setSimpleValue($value)\n    {\n        // Nulling the relationship\n        if (!$value) {\n            if ($this->parent->exists) {\n                $this->parent->bindEventOnce('model.afterSave', function () {\n                    $this->ensureRelationIsEmpty();\n                });\n            }\n            return;\n        }\n\n        if ($value instanceof Model) {\n            $value = new Collection([$value]);\n        }\n\n        if ($value instanceof CollectionBase) {\n            $collection = $value;\n\n            if ($this->parent->exists) {\n                $collection->each(function ($instance) {\n                    $instance->setAttribute($this->getForeignKeyName(), $this->getParentKey());\n                    $instance->setAttribute($this->getMorphType(), $this->morphClass);\n                });\n            }\n        }\n        else {\n            $collection = $this->getRelated()\n                ->whereIn($this->getRelatedKeyName(), (array) $value)\n                ->get()\n            ;\n        }\n\n        if ($collection) {\n            $this->parent->setRelation($this->relationName, $collection);\n\n            $this->parent->bindEventOnce('model.afterSave', function () use ($collection) {\n                $existingIds = $collection->pluck($this->getRelatedKeyName())->all();\n\n                $this->whereNotIn($this->getRelatedKeyName(), $existingIds)->update([\n                    $this->getForeignKeyName() => null,\n                    $this->getMorphType() => null\n                ]);\n\n                $collection->each(function ($instance) {\n                    $instance->setAttribute($this->getForeignKeyName(), $this->getParentKey());\n                    $instance->setAttribute($this->getMorphType(), $this->morphClass);\n                    $instance->save(['timestamps' => false]);\n                });\n            });\n        }\n    }\n\n    /**\n     * getSimpleValue helper for getting this relationship simple value,\n     * generally useful with form values.\n     */\n    public function getSimpleValue()\n    {\n        $value = null;\n        $relationName = $this->relationName;\n\n        if ($this->parent->relationLoaded($relationName)) {\n            $value = $this->parent->getRelation($relationName)\n                ->pluck($this->getRelatedKeyName())\n                ->all()\n            ;\n        }\n        else {\n            $value = $this->query->getQuery()\n                ->pluck($this->getRelatedKeyName())\n                ->all()\n            ;\n        }\n\n        return $value;\n    }\n}\n"
  },
  {
    "path": "src/Database/Relations/MorphOne.php",
    "content": "<?php namespace October\\Rain\\Database\\Relations;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphOne as MorphOneBase;\n\n/**\n * MorphOne\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass MorphOne extends MorphOneBase\n{\n    use MorphOneOrMany;\n    use DefinedConstraints;\n\n    /**\n     * __construct a new has many relationship instance.\n     */\n    public function __construct(Builder $query, Model $parent, $type, $id, $localKey, $relationName = null)\n    {\n        $this->relationName = $relationName;\n\n        parent::__construct($query, $parent, $type, $id, $localKey);\n\n        $this->addDefinedConstraints();\n    }\n\n    /**\n     * setSimpleValue helper for setting this relationship using various expected\n     * values. For example, $model->relation = $value;\n     */\n    public function setSimpleValue($value)\n    {\n        if (is_array($value)) {\n            $value = current($value);\n        }\n\n        // Nulling the relationship\n        if (!$value) {\n            if ($this->parent->exists) {\n                $this->parent->bindEventOnce('model.afterSave', function() {\n                    $this->ensureRelationIsEmpty();\n                });\n            }\n            return;\n        }\n\n        if ($value instanceof Model) {\n            $instance = $value;\n\n            if ($this->parent->exists) {\n                $instance->setAttribute($this->getForeignKeyName(), $this->getParentKey());\n                $instance->setAttribute($this->getMorphType(), $this->morphClass);\n            }\n        }\n        else {\n            $instance = $this->getRelated()->find($value);\n        }\n\n        if ($instance) {\n            $this->parent->setRelation($this->relationName, $instance);\n\n            $this->parent->bindEventOnce('model.afterSave', function () use ($instance) {\n                // Relation is already set, do nothing. This prevents the relationship\n                // from being nulled below and left unset because the save will ignore\n                // attribute values that are numerically equivalent (not dirty).\n                if (\n                    $instance->getOriginal($this->getForeignKeyName()) == $this->getParentKey() &&\n                    $instance->getOriginal($this->getMorphType()) == $this->morphClass\n                ) {\n                    return;\n                }\n\n                $this->update([\n                    $this->getForeignKeyName() => null,\n                    $this->getMorphType() => null\n                ]);\n                $instance->setAttribute($this->getForeignKeyName(), $this->getParentKey());\n                $instance->setAttribute($this->getMorphType(), $this->morphClass);\n                $instance->save(['timestamps' => false]);\n            });\n        }\n    }\n\n    /**\n     * getSimpleValue helper for getting this relationship simple value,\n     * generally useful with form values.\n     */\n    public function getSimpleValue()\n    {\n        $value = null;\n        $relationName = $this->relationName;\n\n        if ($related = $this->parent->$relationName) {\n            $key = $this->getRelatedKeyName();\n            $value = $related->{$key};\n        }\n\n        return $value;\n    }\n}\n"
  },
  {
    "path": "src/Database/Relations/MorphOneOrMany.php",
    "content": "<?php namespace October\\Rain\\Database\\Relations;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Support\\Arr;\n\n/**\n * MorphOneOrMany\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait MorphOneOrMany\n{\n    use DeferOneOrMany;\n\n    /**\n     * @var string relationName is the \"name\" of the relationship.\n     */\n    protected $relationName;\n\n    /**\n     * save the supplied related model with deferred binding support.\n     */\n    public function save(Model $model, $sessionKey = null)\n    {\n        if ($sessionKey === null) {\n            return parent::save($model);\n        }\n\n        $this->add($model, $sessionKey);\n\n        return $model->save() ? $model : false;\n    }\n\n    /**\n     * saveQuietly saves the supplied related model without raising any events,\n     * with deferred binding support.\n     */\n    public function saveQuietly(Model $model, $sessionKey = null)\n    {\n        return Model::withoutEvents(function () use ($model, $sessionKey) {\n            return $this->save($model, $sessionKey);\n        });\n    }\n\n    /**\n     * saveMany saves multiple models with deferred binding support.\n     */\n    public function saveMany($models, $sessionKey = null)\n    {\n        foreach ($models as $model) {\n            $this->save($model, $sessionKey);\n        }\n\n        return $models;\n    }\n\n    /**\n     * saveManyQuietly saves multiple models without raising any events,\n     * with deferred binding support.\n     */\n    public function saveManyQuietly($models, $sessionKey = null)\n    {\n        return Model::withoutEvents(function () use ($models, $sessionKey) {\n            return $this->saveMany($models, $sessionKey);\n        });\n    }\n\n    /**\n     * create a new instance of this related model with deferred binding support.\n     */\n    public function create(array $attributes = [], $sessionKey = null)\n    {\n        $model = parent::create($attributes);\n\n        if ($sessionKey !== null) {\n            $this->add($model, $sessionKey);\n        }\n\n        return $model;\n    }\n\n    /**\n     * createQuietly creates a new instance without raising any events,\n     * with deferred binding support.\n     */\n    public function createQuietly(array $attributes = [], $sessionKey = null)\n    {\n        return Model::withoutEvents(function () use ($attributes, $sessionKey) {\n            return $this->create($attributes, $sessionKey);\n        });\n    }\n\n    /**\n     * forceCreateQuietly creates a new instance bypassing mass assignment\n     * without raising any events, with deferred binding support.\n     */\n    public function forceCreateQuietly(array $attributes = [], $sessionKey = null)\n    {\n        return Model::withoutEvents(function () use ($attributes, $sessionKey) {\n            $model = parent::forceCreate($attributes);\n\n            if ($sessionKey !== null) {\n                $this->add($model, $sessionKey);\n            }\n\n            return $model;\n        });\n    }\n\n    /**\n     * createMany creates multiple related models with deferred binding support.\n     */\n    public function createMany(iterable $records, $sessionKey = null)\n    {\n        $instances = parent::createMany($records);\n\n        if ($sessionKey !== null) {\n            foreach ($instances as $model) {\n                $this->add($model, $sessionKey);\n            }\n        }\n\n        return $instances;\n    }\n\n    /**\n     * createManyQuietly creates multiple models without raising any events,\n     * with deferred binding support.\n     */\n    public function createManyQuietly(iterable $records, $sessionKey = null)\n    {\n        return Model::withoutEvents(function () use ($records, $sessionKey) {\n            return $this->createMany($records, $sessionKey);\n        });\n    }\n\n    /**\n     * createOrFirst attempts to create the record, or if a unique constraint\n     * violation occurs, finds the existing record.\n     */\n    public function createOrFirst(array $attributes = [], \\Closure|array $values = [], $sessionKey = null)\n    {\n        $model = parent::createOrFirst($attributes, $values);\n\n        if ($sessionKey !== null) {\n            $this->add($model, $sessionKey);\n        }\n\n        return $model;\n    }\n\n    /**\n     * add a model to this relationship type.\n     */\n    public function add(Model $model, $sessionKey = null)\n    {\n        if ($sessionKey === null) {\n            /**\n             * @event model.relation.beforeAdd\n             * Called before adding a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)\n             *\n             * Example usage:\n             *\n             *     $model->bindEvent('model.relation.beforeAdd', function (string $relationName, \\October\\Rain\\Database\\Model $relatedModel) use (\\October\\Rain\\Database\\Model $model) {\n             *         if ($relationName === 'some_relation') {\n             *             return false;\n             *         }\n             *     });\n             *\n             */\n            if ($this->parent->fireEvent('model.relation.beforeAdd', [$this->relationName, $model], true) === false) {\n                return;\n            }\n\n            // Associate the model\n            if ($this->parent->exists) {\n                $model->setAttribute($this->getForeignKeyName(), $this->getParentKey());\n                $model->setAttribute($this->getMorphType(), $this->morphClass);\n                $model->save();\n            }\n            else {\n                $this->parent->bindEventOnce('model.afterSave', function () use ($model) {\n                    $model->setAttribute($this->getForeignKeyName(), $this->getParentKey());\n                    $model->setAttribute($this->getMorphType(), $this->morphClass);\n                    $model->save();\n                });\n            }\n\n            // Use the opportunity to set the relation in memory\n            if ($this instanceof MorphOne) {\n                $this->parent->setRelation($this->relationName, $model);\n            }\n            else {\n                $this->parent->unsetRelation($this->relationName);\n            }\n\n            /**\n             * @event model.relation.add\n             * Called after adding a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)\n             *\n             * Example usage:\n             *\n             *     $model->bindEvent('model.relation.add', function (string $relationName, \\October\\Rain\\Database\\Model $relatedModel) use (\\October\\Rain\\Database\\Model $model) {\n             *         $relatedClass = get_class($relatedModel);\n             *         $modelClass = get_class($model);\n             *         traceLog(\"{$relatedClass} was added as {$relationName} to {$modelClass}.\");\n             *     });\n             *\n             */\n            $this->parent->fireEvent('model.relation.add', [$this->relationName, $model]);\n        }\n        else {\n            $this->parent->bindDeferred($this->relationName, $model, $sessionKey);\n        }\n    }\n\n    /**\n     * remove a model from this relationship type.\n     */\n    public function remove(Model $model, $sessionKey = null)\n    {\n        if ($sessionKey === null) {\n            /**\n             * @event model.relation.beforeRemove\n             * Called before removing a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)\n             *\n             * Example usage:\n             *\n             *     $model->bindEvent('model.relation.beforeRemove', function (string $relationName, \\October\\Rain\\Database\\Model $relatedModel) use (\\October\\Rain\\Database\\Model $model) {\n             *         if ($relationName === 'perm_relation') {\n             *             return false;\n             *         }\n             *     });\n             *\n             */\n            if ($this->parent->fireEvent('model.relation.beforeRemove', [$this->relationName, $model], true) === false) {\n                return;\n            }\n\n            if (!$this->isModelRemovable($model)) {\n                return;\n            }\n\n            $options = $this->parent->getRelationDefinition($this->relationName);\n\n            // Delete or orphan the model\n            if (Arr::get($options, 'delete', false)) {\n                $model->delete();\n            }\n            else {\n                $model->setAttribute($this->getForeignKeyName(), null);\n                $model->setAttribute($this->getMorphType(), null);\n                $model->save();\n            }\n\n            // Use this opportunity to set the relation in memory\n            if ($this instanceof MorphOne) {\n                $this->parent->setRelation($this->relationName, null);\n            }\n            else {\n                $this->parent->unsetRelation($this->relationName);\n            }\n\n            /**\n             * @event model.relation.remove\n             * Called after removing a relation to the model (for AttachOneOrMany, HasOneOrMany & MorphOneOrMany relations)\n             *\n             * Example usage:\n             *\n             *     $model->bindEvent('model.relation.remove', function (string $relationName, \\October\\Rain\\Database\\Model $relatedModel) use (\\October\\Rain\\Database\\Model $model) {\n             *         $relatedClass = get_class($relatedModel);\n             *         $modelClass = get_class($model);\n             *         traceLog(\"{$relatedClass} was removed from {$modelClass}.\");\n             *     });\n             *\n             */\n            $this->parent->fireEvent('model.relation.remove', [$this->relationName, $model]);\n        }\n        else {\n            $this->parent->unbindDeferred($this->relationName, $model, $sessionKey);\n        }\n    }\n\n    /**\n     * isModelRemovable returns true if an existing model is already associated\n     */\n    protected function isModelRemovable($model): bool\n    {\n        return\n            ((string) $model->getAttribute($this->getForeignKeyName()) === (string) $this->getParentKey()) &&\n            $model->getAttribute($this->getMorphType()) === $this->morphClass;\n    }\n\n    /**\n     * ensureRelationIsEmpty ensures the relation is empty, either deleted or nulled.\n     */\n    protected function ensureRelationIsEmpty()\n    {\n        $options = $this->parent->getRelationDefinition($this->relationName);\n\n        if (Arr::get($options, 'delete', false)) {\n            $this->delete();\n        }\n        else {\n            $this->update([\n                $this->getForeignKeyName() => null,\n                $this->getMorphType() => null\n            ]);\n        }\n    }\n\n    /**\n     * getRelatedKeyName\n     * @return string\n     */\n    public function getRelatedKeyName()\n    {\n        return $this->related->getKeyName();\n    }\n}\n"
  },
  {
    "path": "src/Database/Relations/MorphTo.php",
    "content": "<?php namespace October\\Rain\\Database\\Relations;\n\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse Illuminate\\Database\\Eloquent\\Relations\\MorphTo as MorphToBase;\n\n/**\n * MorphTo\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass MorphTo extends MorphToBase\n{\n    use DefinedConstraints;\n\n    /**\n     * @var string relationName is the \"name\" of the relationship.\n     */\n    protected $relationName;\n\n    /**\n     * __construct relationship\n     */\n    public function __construct(Builder $query, Model $parent, $foreignKey, $otherKey, $type, $relationName)\n    {\n        $this->relationName = $relationName;\n\n        parent::__construct($query, $parent, $foreignKey, $otherKey, $type, $relationName);\n\n        $this->addDefinedConstraints();\n    }\n\n    /**\n     * associate the model instance to the given parent.\n     *\n     * @param  \\Illuminate\\Database\\Eloquent\\Model  $model\n     * @return \\Illuminate\\Database\\Eloquent\\Model\n     */\n    public function associate($model)\n    {\n        /**\n         * @event model.relation.beforeAssociate\n         * Called before associating a relation to the model (only for BelongsTo/MorphTo relations)\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.relation.beforeAssociate', function (string $relationName, \\October\\Rain\\Database\\Model $relatedModel) use (\\October\\Rain\\Database\\Model $model) {\n         *         if ($relationName === 'some_relation') {\n         *             return false;\n         *         }\n         *     });\n         *\n         */\n        if ($this->parent->fireEvent('model.relation.beforeAssociate', [$this->relationName, $model], true) === false) {\n            return;\n        }\n\n        $result = parent::associate($model);\n\n        /**\n         * @event model.relation.associate\n         * Called after associating a relation to the model (only for BelongsTo/MorphTo relations)\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.relation.associate', function (string $relationName, \\October\\Rain\\Database\\Model $relatedModel) use (\\October\\Rain\\Database\\Model $model) {\n         *         $relatedClass = get_class($relatedModel);\n         *         $modelClass = get_class($model);\n         *         traceLog(\"{$relatedClass} was associated as {$relationName} to {$modelClass}.\");\n         *     });\n         *\n         */\n        $this->parent->fireEvent('model.relation.associate', [$this->relationName, $model]);\n\n        return $result;\n    }\n\n    /**\n     * dissociate previously dissociated model from the given parent.\n     *\n     * @return \\Illuminate\\Database\\Eloquent\\Model\n     */\n    public function dissociate()\n    {\n        /**\n         * @event model.relation.beforeDissociate\n         * Called before dissociating a relation to the model (only for BelongsTo/MorphTo relations)\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.relation.beforeDissociate', function (string $relationName) use (\\October\\Rain\\Database\\Model $model) {\n         *         if ($relationName === 'perm_relation') {\n         *             return false;\n         *         }\n         *     });\n         *\n         */\n        if ($this->parent->fireEvent('model.relation.beforeDissociate', [$this->relationName], true) === false) {\n            return;\n        }\n\n        $result = parent::dissociate();\n\n        /**\n         * @event model.relation.dissociate\n         * Called after dissociating a relation to the model (only for BelongsTo/MorphTo relations)\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.relation.dissociate', function (string $relationName) use (\\October\\Rain\\Database\\Model $model) {\n         *         $modelClass = get_class($model);\n         *         traceLog(\"{$relationName} was dissociated from {$modelClass}.\");\n         *     });\n         *\n         */\n        $this->parent->fireEvent('model.relation.dissociate', [$this->relationName]);\n\n        return $result;\n    }\n\n    /**\n     * setSimpleValue is a helper for setting this relationship using various expected\n     * values. For example, $model->relation = $value;\n     */\n    public function setSimpleValue($value)\n    {\n        // Nulling the relationship\n        if (!$value) {\n            $this->dissociate();\n            return;\n        }\n\n        if ($value instanceof Model) {\n            // Non existent model, use a single serve event to associate it again when ready\n            if (!$value->exists) {\n                $value->bindEventOnce('model.afterSave', function () use ($value) {\n                    $this->associate($value);\n                });\n            }\n\n            $this->associate($value);\n            $this->parent->setRelation($this->relationName, $value);\n        }\n        elseif (is_array($value)) {\n            [$modelId, $modelClass] = $value;\n            $this->parent->setAttribute($this->foreignKey, $modelId);\n            $this->parent->setAttribute($this->morphType, $modelClass);\n            $this->parent->unsetRelation($this->relationName);\n        }\n        else {\n            $this->parent->setAttribute($this->foreignKey, $value);\n            $this->parent->unsetRelation($this->relationName);\n        }\n    }\n\n    /**\n     * getSimpleValue is a helper for getting this relationship simple value,\n     * generally useful with form values.\n     */\n    public function getSimpleValue()\n    {\n        return [\n            $this->parent->getAttribute($this->foreignKey),\n            $this->parent->getAttribute($this->morphType)\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Database/Relations/MorphToMany.php",
    "content": "<?php namespace October\\Rain\\Database\\Relations;\n\nuse Illuminate\\Support\\Arr;\nuse Illuminate\\Database\\Eloquent\\Model;\nuse Illuminate\\Database\\Eloquent\\Builder;\nuse October\\Rain\\Database\\MorphPivot;\n\n/**\n * MorphToMany\n *\n * This class is a carbon copy of Illuminate\\Database\\Eloquent\\Relations\\MorphToMany\n * so the base October\\Rain\\Database\\Relations\\BelongsToMany class can be inherited\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass MorphToMany extends BelongsToMany\n{\n    use DefinedConstraints;\n\n    /**\n     * @var string morphType is type of the polymorphic relation\n     */\n    protected $morphType;\n\n    /**\n     * @var string morphClass is the class name of the morph type constraint\n     */\n    protected $morphClass;\n\n    /**\n     * @var bool inverse indicates if we are connecting the inverse of the relation.\n     * This primarily affects the morphClass constraint.\n     */\n    protected $inverse;\n\n    /**\n     * __construct will create a new morph to many relationship instance\n     * @param  string  $name\n     * @param  string  $table\n     * @param  string  $foreignKey\n     * @param  string  $otherKey\n     * @param  string  $relationName\n     * @param  bool  $inverse\n     */\n    public function __construct(\n        Builder $query,\n        Model $parent,\n        $name,\n        $table,\n        $foreignKey,\n        $otherKey,\n        $parentKey,\n        $relatedKey,\n        $relationName = null,\n        $inverse = false\n    ) {\n        $this->inverse = $inverse;\n\n        $this->morphType = $name.'_type';\n\n        $this->morphClass = $inverse ? $query->getModel()->getMorphClass() : $parent->getMorphClass();\n\n        parent::__construct(\n            $query,\n            $parent,\n            $table,\n            $foreignKey,\n            $otherKey,\n            $parentKey,\n            $relatedKey,\n            $relationName\n        );\n\n        $this->addDefinedConstraints();\n    }\n\n    /**\n     * addWhereConstraints set the where clause for the relation query\n     * @return $this\n     */\n    protected function addWhereConstraints()\n    {\n        parent::addWhereConstraints();\n\n        $this->query->where($this->table.'.'.$this->morphType, $this->morphClass);\n\n        return $this;\n    }\n\n    /**\n     * addEagerConstraints sets the constraints for an eager load of the relation\n     * @param  array  $models\n     * @return void\n     */\n    public function addEagerConstraints(array $models)\n    {\n        parent::addEagerConstraints($models);\n\n        $this->query->where($this->table.'.'.$this->morphType, $this->morphClass);\n    }\n\n    /**\n     * baseAttachRecord creates a new pivot attachment record.\n     * @param  int   $id\n     * @param  bool  $timed\n     * @return array\n     */\n    protected function baseAttachRecord($id, $timed)\n    {\n        return Arr::add(\n            parent::baseAttachRecord($id, $timed),\n            $this->morphType,\n            $this->morphClass\n        );\n    }\n\n    /**\n     * getRelationExistenceQuery adds the constraints for a relationship count query.\n     * @param  \\Illuminate\\Database\\Eloquent\\Builder  $query\n     * @param  \\Illuminate\\Database\\Eloquent\\Builder  $parentQuery\n     * @param  array|mixed  $columns\n     * @return \\Illuminate\\Database\\Eloquent\\Builder\n     */\n    public function getRelationExistenceQuery(Builder $query, Builder $parentQuery, $columns = ['*'])\n    {\n        return parent::getRelationExistenceQuery($query, $parentQuery, $columns)->where(\n            $this->table.'.'.$this->morphType,\n            $this->morphClass\n        );\n    }\n\n    /**\n     * newPivotQuery creates a new query builder for the pivot table.\n     */\n    public function newPivotQuery()\n    {\n        return parent::newPivotQuery()->where($this->morphType, $this->morphClass);\n    }\n\n    /**\n     * newPivot creates a new pivot model instance\n     * @param  array  $attributes\n     * @param  bool   $exists\n     * @return \\Illuminate\\Database\\Eloquent\\Relations\\Pivot\n     */\n    public function newPivot(array $attributes = [], $exists = false)\n    {\n        // October looks to the relationship parent\n        $pivot = $this->parent->newRelationPivot($this->relationName, $this->parent, $attributes, $this->table, $exists);\n\n        // Laravel creates new pivot model this way\n        if (empty($pivot)) {\n            $using = $this->using;\n\n            $pivot = $using\n                ? $using::fromRawAttributes($this->parent, $attributes, $this->table, $exists)\n                : MorphPivot::fromAttributes($this->parent, $attributes, $this->table, $exists);\n        }\n\n        $pivot->setPivotKeys($this->foreignPivotKey, $this->relatedPivotKey)\n              ->setMorphType($this->morphType)\n              ->setMorphClass($this->morphClass);\n\n        return $pivot;\n    }\n\n    /**\n     * getMorphType gets the foreign key \"type\" name\n     */\n    public function getMorphType()\n    {\n        return $this->morphType;\n    }\n\n    /**\n     * getMorphClass get the class name of the parent model\n     */\n    public function getMorphClass()\n    {\n        return $this->morphClass;\n    }\n}\n"
  },
  {
    "path": "src/Database/Relations/Relation.php",
    "content": "<?php namespace October\\Rain\\Database\\Relations;\n\nuse Illuminate\\Database\\Eloquent\\Relations\\Relation as RelationBase;\n\n/**\n * Relation is an umbrella class for Laravel.\n *\n *     Relation::morphMap([\n *         'posts' => 'App\\Post',\n *         'videos' => 'App\\Video',\n *     ]);\n *\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nabstract class Relation extends RelationBase\n{\n}\n"
  },
  {
    "path": "src/Database/Replicator.php",
    "content": "<?php namespace October\\Rain\\Database;\n\nuse October\\Rain\\Support\\Arr;\nuse Illuminate\\Database\\Eloquent\\Model as EloquentModel;\nuse Illuminate\\Database\\Eloquent\\Collection as CollectionBase;\nuse Illuminate\\Database\\Eloquent\\Relations\\HasOneOrMany;\n\n/**\n * Replicator service to duplicating and replicating model records\n */\nclass Replicator\n{\n    /**\n     * @var \\Model model context\n     */\n    protected $model;\n\n    /**\n     * @var bool isDuplicating the record or just returning a non-existing instance\n     */\n    protected $isDuplicating = false;\n\n    /**\n     * @var bool isMultisite context\n     */\n    protected $isMultisite = false;\n\n    /**\n     * @var array associationMap from original record to newly created record\n     */\n    protected $associationMap = [];\n\n    /**\n     * __construct\n     */\n    public function __construct($model)\n    {\n        $this->model = $model;\n        $this->isMultisite = $model->isClassInstanceOf(\\October\\Contracts\\Database\\MultisiteInterface::class);\n    }\n\n    /**\n     * replicate replicates the model into a new, non-existing instance,\n     * including replicating relations.\n     *\n     * @param  array|null  $except\n     * @return static\n     */\n    public function replicate(?array $except = null)\n    {\n        $this->isDuplicating = false;\n\n        return $this->replicateRelationsInternal($except);\n    }\n\n    /**\n     * duplicate replicates a model with special multisite duplication logic.\n     * To avoid duplication of has many relations, the logic only propagates relations on\n     * the parent model since they are shared via site_root_id beyond this point.\n     *\n     * @param  array|null  $except\n     * @return static\n     */\n    public function duplicate(?array $except = null)\n    {\n        $this->isDuplicating = true;\n\n        return $this->replicateRelationsInternal($except);\n    }\n\n    /**\n     * replicateRelationsInternal\n     */\n    protected function replicateRelationsInternal(?array $except = null)\n    {\n        $defaults = [\n            $this->model->getKeyName(),\n            $this->model->getCreatedAtColumn(),\n            $this->model->getUpdatedAtColumn(),\n        ];\n\n        if ($this->isMultisite) {\n            $defaults[] = 'site_root_id';\n        }\n\n        $attributes = Arr::except(\n            $this->model->attributes,\n            $except ? array_unique(array_merge($except, $defaults)) : $defaults\n        );\n\n        $instance = $this->model->newReplicationInstance($attributes);\n\n        $definitions = $this->model->getRelationDefinitions();\n\n        foreach ($definitions as $type => $relations) {\n            foreach ($relations as $name => $options) {\n                if ($this->isRelationReplicable($name)) {\n                    $this->replicateRelationInternal($instance->$name(), $this->model->$name);\n                }\n            }\n        }\n\n        return $instance;\n    }\n\n    /**\n     * replicateRelationInternal on the model instance with the supplied ones\n     */\n    protected function replicateRelationInternal($relationObject, $models)\n    {\n        if ($models instanceof CollectionBase) {\n            $models = $models->all();\n        }\n        elseif ($models instanceof EloquentModel) {\n            $models = [$models];\n        }\n        else {\n            $models = (array) $models;\n        }\n\n        $this->associationMap = [];\n        foreach (array_filter($models) as $model) {\n            if ($relationObject instanceof HasOneOrMany) {\n                $relationObject->add($newModel = $model->replicateWithRelations());\n                $this->mapAssociation($model, $newModel);\n            }\n            else {\n                $relationObject->add($model);\n            }\n        }\n\n        $relatedModel = $relationObject->getRelated();\n        if ($relatedModel->isClassInstanceOf(\\October\\Contracts\\Database\\TreeInterface::class)) {\n            $this->updateTreeAssociations();\n        }\n    }\n\n    /**\n     * isRelationReplicable determines whether the specified relation should be replicated\n     * when replicateWithRelations() is called instead of save() on the model. Default: true.\n     */\n    protected function isRelationReplicable(string $name): bool\n    {\n        // Relation is shared via propagation\n        if (\n            !$this->isDuplicating &&\n            $this->isMultisite &&\n            $this->model->isAttributePropagatable($name)\n        ) {\n            return false;\n        }\n\n        return $this->model->isRelationReplicable($name);\n    }\n\n    /**\n     * mapAssociation is an internal method that keeps a record of what records were created\n     * and their associated source, the following format is used:\n     *\n     *     [FromModel::id] => [FromModel, ToModel]\n     */\n    protected function mapAssociation($currentModel, $replicatedModel)\n    {\n        $this->associationMap[$currentModel->getKey()] = [$currentModel, $replicatedModel];\n    }\n\n    /**\n     * updateTreeAssociations sets new parents on the replicated records\n     */\n    protected function updateTreeAssociations()\n    {\n        foreach ($this->associationMap as $tuple) {\n            [$currentModel, $replicatedModel] = $tuple;\n            $newParent = $this->associationMap[$currentModel->getParentId()][1] ?? null;\n            $replicatedModel->parent = $newParent;\n        }\n    }\n}\n"
  },
  {
    "path": "src/Database/Schema/Blueprint.php",
    "content": "<?php namespace October\\Rain\\Database\\Schema;\n\nuse Illuminate\\Database\\Schema\\Blueprint as BaseBlueprint;\n\n/**\n * Blueprint proxy class\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Blueprint extends BaseBlueprint\n{\n    /**\n     * multisite adds columns used by the Multisite trait\n     *\n     * @param  string  $column\n     * @param  string|null  $indexName\n     * @return void\n     */\n    public function multisite($column = 'site_id', $indexName = null)\n    {\n        $this->unsignedBigInteger($column)->nullable();\n\n        $this->unsignedBigInteger('site_root_id')->nullable();\n\n        $this->index([$column, 'site_root_id'], $indexName);\n    }\n}\n"
  },
  {
    "path": "src/Database/Scopes/MultisiteGroupScope.php",
    "content": "<?php namespace October\\Rain\\Database\\Scopes;\n\nuse Site;\nuse Illuminate\\Database\\Eloquent\\Model as ModelBase;\nuse Illuminate\\Database\\Eloquent\\Scope as ScopeInterface;\nuse Illuminate\\Database\\Eloquent\\Builder as BuilderBase;\n\n/**\n * MultisiteGroupScope applies site group scoping to models that use\n * the MultisiteGroup trait. Unlike MultisiteScope which filters by\n * individual site_id, this scope filters by site_group_id to scope\n * records to a tenant.\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass MultisiteGroupScope implements ScopeInterface\n{\n    /**\n     * @var array extensions to be added to the builder.\n     */\n    protected $extensions = ['WithSiteGroup', 'WithSiteGroups'];\n\n    /**\n     * apply the scope to a given Eloquent query builder.\n     */\n    public function apply(BuilderBase $builder, ModelBase $model)\n    {\n        if ($model->isMultisiteGroupEnabled() && !Site::hasGlobalContext()) {\n            $builder->where(\n                $model->getQualifiedSiteGroupIdColumn(),\n                Site::getSiteGroupIdFromContext()\n            );\n        }\n    }\n\n    /**\n     * extend the Eloquent query builder.\n     */\n    public function extend(BuilderBase $builder)\n    {\n        foreach ($this->extensions as $extension) {\n            $this->{\"add{$extension}\"}($builder);\n        }\n    }\n\n    /**\n     * addWithSiteGroup removes this scope and includes the specified site group\n     */\n    protected function addWithSiteGroup(BuilderBase $builder)\n    {\n        $builder->macro('withSiteGroup', function (BuilderBase $builder, $groupId) {\n            return $builder\n                ->withoutGlobalScope($this)\n                ->where($builder->getModel()->getQualifiedSiteGroupIdColumn(), $groupId)\n            ;\n        });\n    }\n\n    /**\n     * addWithSiteGroups removes this scope and includes everything,\n     * or filters by an array of group ids.\n     */\n    protected function addWithSiteGroups(BuilderBase $builder)\n    {\n        $builder->macro('withSiteGroups', function (BuilderBase $builder, $groupIds = null) {\n            if (!is_array($groupIds)) {\n                return $builder->withoutGlobalScope($this);\n            }\n\n            return $builder\n                ->withoutGlobalScope($this)\n                ->whereIn($builder->getModel()->getQualifiedSiteGroupIdColumn(), $groupIds)\n            ;\n        });\n    }\n}\n"
  },
  {
    "path": "src/Database/Scopes/MultisiteScope.php",
    "content": "<?php namespace October\\Rain\\Database\\Scopes;\n\nuse Site;\nuse Illuminate\\Database\\Eloquent\\Model as ModelBase;\nuse Illuminate\\Database\\Eloquent\\Scope as ScopeInterface;\nuse Illuminate\\Database\\Eloquent\\Builder as BuilderBase;\n\n/**\n * MultisiteScope\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass MultisiteScope implements ScopeInterface\n{\n    /**\n     * @var array extensions to be added to the builder.\n     */\n    protected $extensions = ['WithSite', 'WithSites', 'WithSyncSites'];\n\n    /**\n     * apply the scope to a given Eloquent query builder.\n     */\n    public function apply(BuilderBase $builder, ModelBase $model)\n    {\n        if ($model->isMultisiteEnabled() && !Site::hasGlobalContext()) {\n            $builder->where($model->getQualifiedSiteIdColumn(), Site::getSiteIdFromContext());\n        }\n    }\n\n    /**\n     * extend the Eloquent query builder.\n     */\n    public function extend(BuilderBase $builder)\n    {\n        foreach ($this->extensions as $extension) {\n            $this->{\"add{$extension}\"}($builder);\n        }\n    }\n\n    /**\n     * addWithSite removes this scope and includes the specified site\n     */\n    protected function addWithSite(BuilderBase $builder)\n    {\n        $builder->macro('withSite', function (BuilderBase $builder, $siteId) {\n            return $builder\n                ->withoutGlobalScope($this)\n                ->where($builder->getModel()->getQualifiedSiteIdColumn(), $siteId)\n            ;\n        });\n    }\n\n    /**\n     * addWithSites removes this scope and includes everything.\n     */\n    protected function addWithSites(BuilderBase $builder)\n    {\n        $builder->macro('withSites', function (BuilderBase $builder, $siteIds = null) {\n            if (!is_array($siteIds)) {\n                return $builder->withoutGlobalScope($this);\n            }\n\n            return $builder\n                ->withoutGlobalScope($this)\n                ->whereIn($builder->getModel()->getQualifiedSiteIdColumn(), $siteIds)\n            ;\n        });\n    }\n\n    /**\n     * addWithSyncSites removes this scope and includes sites that should be synced with this model\n     */\n    protected function addWithSyncSites(BuilderBase $builder)\n    {\n        $builder->macro('withSyncSites', function (BuilderBase $builder) {\n            return $builder->withSites($builder->getModel()->getMultisiteSyncSites());\n        });\n    }\n}\n"
  },
  {
    "path": "src/Database/Scopes/NestedTreeScope.php",
    "content": "<?php namespace October\\Rain\\Database\\Scopes;\n\nuse Illuminate\\Database\\Eloquent\\Model as ModelBase;\nuse Illuminate\\Database\\Eloquent\\Scope as ScopeInterface;\nuse Illuminate\\Database\\Eloquent\\Builder as BuilderBase;\n\n/**\n * NestedTreeScope\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass NestedTreeScope implements ScopeInterface\n{\n    /**\n     * apply the scope to a given Eloquent query builder.\n     */\n    public function apply(BuilderBase $builder, ModelBase $model)\n    {\n        $builder->getQuery()->orderBy($model->getLeftColumnName());\n    }\n\n    /**\n     * extend the Eloquent query builder.\n     */\n    public function extend(BuilderBase $builder)\n    {\n        $removeOnMethods = ['reorder', 'orderBy', 'groupBy'];\n\n        foreach ($removeOnMethods as $method) {\n            $builder->macro($method, function ($builder, ...$args) use ($method) {\n                $builder\n                    ->withoutGlobalScope($this)\n                    ->getQuery()\n                    ->$method(...$args)\n                ;\n\n                return $builder;\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/Database/Scopes/SoftDeleteScope.php",
    "content": "<?php namespace October\\Rain\\Database\\Scopes;\n\nuse Illuminate\\Database\\Eloquent\\SoftDeletingScope;\nuse Illuminate\\Database\\Eloquent\\Model as ModelBase;\nuse Illuminate\\Database\\Eloquent\\Builder as BuilderBase;\n\n/**\n * SoftDeleteScope\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass SoftDeleteScope extends SoftDeletingScope\n{\n    /**\n     * apply the scope to a given Eloquent query builder.\n     */\n    public function apply(BuilderBase $builder, ModelBase $model)\n    {\n        if ($model->isSoftDeleteEnabled()) {\n            $builder->whereNull($model->getQualifiedDeletedAtColumn());\n        }\n    }\n}\n"
  },
  {
    "path": "src/Database/Scopes/SortableScope.php",
    "content": "<?php namespace October\\Rain\\Database\\Scopes;\n\nuse Illuminate\\Database\\Eloquent\\Model as ModelBase;\nuse Illuminate\\Database\\Eloquent\\Scope as ScopeInterface;\nuse Illuminate\\Database\\Eloquent\\Builder as BuilderBase;\n\n/**\n * SortableScope will apply default sort ordering\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass SortableScope implements ScopeInterface\n{\n    /**\n     * apply the scope to a given Eloquent query builder.\n     */\n    public function apply(BuilderBase $builder, ModelBase $model)\n    {\n        $builder->getQuery()->orderBy($model->getQualifiedSortOrderColumn());\n    }\n\n    /**\n     * extend the Eloquent query builder.\n     */\n    public function extend(BuilderBase $builder)\n    {\n        $removeOnMethods = ['reorder', 'orderBy', 'groupBy'];\n\n        foreach ($removeOnMethods as $method) {\n            $builder->macro($method, function ($builder, ...$args) use ($method) {\n                $builder\n                    ->withoutGlobalScope($this)\n                    ->getQuery()\n                    ->$method(...$args)\n                ;\n\n                return $builder;\n            });\n        }\n    }\n}\n"
  },
  {
    "path": "src/Database/SortableScope.php",
    "content": "<?php namespace October\\Rain\\Database;\n\n/**\n * SortableScope\n *\n * @deprecated\n * @see \\October\\Rain\\Database\\Scopes\\SortableScope\n */\nclass SortableScope extends \\October\\Rain\\Database\\Scopes\\SortableScope\n{\n}\n"
  },
  {
    "path": "src/Database/Traits/BaseIdentifier.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\n/**\n * BaseIdentifier trait adds random base64 identifiers to a model used as a random\n * lookup key that is immune to enumeration attacks. The model is assumed to have\n * the attribute: baseid.\n *\n * Add this to your database table with:\n *\n *     $table->string('baseid')->nullable()->index();\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait BaseIdentifier\n{\n    /**\n     * initializeBaseIdentifier trait for a model.\n     */\n    public function initializeBaseIdentifier()\n    {\n        $this->bindEvent('model.saveInternal', function () {\n            $this->baseIdentifyAttributes();\n        });\n    }\n\n    /**\n     * baseIdentifyAttributes\n     */\n    public function baseIdentifyAttributes()\n    {\n        $baseidAttribute = $this->getBaseIdentifierColumnName();\n        if (!$this->{$baseidAttribute}) {\n            $this->attributes[$baseidAttribute] = $this->getBaseIdentifierUniqueAttributeValue($baseidAttribute);\n        }\n    }\n\n    /**\n     * generateBaseIdentifier returns a random encoded 64 bit number\n     */\n    public function generateBaseIdentifier()\n    {\n        return rtrim(strtr(base64_encode(random_bytes(8)), '+/', '-_'), '=');\n    }\n\n    /**\n     * getBaseIdentifierUniqueAttributeValue ensures a unique attribute value, if the value\n     * is already used another base identifier is created. Returns a safe value that is unique.\n     * @param string $name\n     * @return string\n     */\n    protected function getBaseIdentifierUniqueAttributeValue($name)\n    {\n        $value = $this->generateBaseIdentifier();\n\n        while ($this->newQueryWithoutScopes()->where($name, $value)->count() > 0) {\n            $value = $this->generateBaseIdentifier();\n        }\n\n        return $value;\n    }\n\n    /**\n     * getBaseIdentifierColumnName gets the name of the \"baseid\" column.\n     * @return string\n     */\n    public function getBaseIdentifierColumnName()\n    {\n        return defined('static::BASEID') ? static::BASEID : 'baseid';\n    }\n}\n"
  },
  {
    "path": "src/Database/Traits/Defaultable.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\n/**\n * Defaultable adds default assignment to models\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait Defaultable\n{\n    /**\n     * @var static defaultableCache\n     */\n    protected static $defaultableCache;\n\n    /**\n     * initializeDefaultable\n     */\n    public function initializeDefaultable()\n    {\n        $this->bindEvent('model.afterSave', [$this, 'defaultableAfterSave']);\n    }\n\n    /**\n     * defaultableAfterSave\n     */\n    public function defaultableAfterSave()\n    {\n        if ($this->is_default) {\n            $this->makeDefault();\n        }\n    }\n\n    /**\n     * makeDefault\n     */\n    public function makeDefault()\n    {\n        $this->newQuery()->where('id', $this->id)->update(['is_default' => true]);\n        $this->newQuery()->where('id', '<>', $this->id)->update(['is_default' => false]);\n    }\n\n    /**\n     * clearDefaultableCache clears the default record cache\n     */\n    public static function clearDefaultableCache()\n    {\n        static::$defaultableCache = null;\n    }\n\n    /**\n     * getDefault returns the default product type.\n     */\n    public static function getDefault()\n    {\n        if (static::$defaultableCache !== null) {\n            return static::$defaultableCache;\n        }\n\n        $defaultType = static::where('is_default', true)->first();\n\n        // If no default is found, find the first record and make it the default.\n        if (!$defaultType && ($defaultType = static::first())) {\n            $defaultType->makeDefault();\n        }\n\n        return static::$defaultableCache = $defaultType;\n    }\n}\n"
  },
  {
    "path": "src/Database/Traits/DeferredBinding.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\nuse October\\Rain\\Database\\Models\\DeferredBinding as DeferredBindingModel;\n\n/**\n * DeferredBinding trait is implemented by all models\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait DeferredBinding\n{\n    /**\n     * @var string sessionKey is a unique session key used for deferred binding\n     */\n    public $sessionKey;\n\n    /**\n     * isDeferrable returns true if a relation exists and can be deferred\n     */\n    public function isDeferrable($relationName): bool\n    {\n        if (!$this->hasRelation($relationName)) {\n            return false;\n        }\n\n        return in_array(\n            $this->getRelationType($relationName),\n            $this->getDeferrableRelationTypes()\n        );\n    }\n\n    /**\n     * hasDeferred returns true if a deferred record exists for a relation\n     */\n    public function hasDeferred($sessionKey = null, $relationName = null): bool\n    {\n        if ($sessionKey === null) {\n            $sessionKey = $this->sessionKey;\n        }\n\n        return DeferredBindingModel::hasDeferredActions(get_class($this), $sessionKey, $relationName);\n    }\n\n    /**\n     * bindDeferred binds a deferred relationship to the supplied record\n     */\n    public function bindDeferred($relation, $record, $sessionKey, $pivotData = []): DeferredBindingModel\n    {\n        $binding = new DeferredBindingModel;\n        $binding->setConnection($this->getConnectionName());\n        $binding->master_type = get_class($this);\n        $binding->master_field = $relation;\n        $binding->slave_type = get_class($record);\n        $binding->slave_id = $record->getKey();\n        $binding->pivot_data = $pivotData;\n        $binding->session_key = $sessionKey;\n        $binding->is_bind = true;\n\n        /**\n         * @event deferredBinding.newBindInstance\n         * Called after the DeferredBindingModel is initialized for binding\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('deferredBinding.newBindInstance', function ((\\Model) $model) {\n         *         $model->some_attribute = true;\n         *     });\n         *\n         */\n        if ($event = $this->fireEvent('deferredBinding.newBindInstance', $binding, true)) {\n            $binding = $event;\n        }\n\n        $binding->save();\n\n        return $binding;\n    }\n\n    /**\n     * unbindDeferred unbinds a deferred relationship to the supplied record\n     */\n    public function unbindDeferred($relation, $record, $sessionKey): DeferredBindingModel\n    {\n        $binding = new DeferredBindingModel;\n        $binding->setConnection($this->getConnectionName());\n        $binding->master_type = get_class($this);\n        $binding->master_field = $relation;\n        $binding->slave_type = get_class($record);\n        $binding->slave_id = $record->getKey();\n        $binding->session_key = $sessionKey;\n        $binding->is_bind = false;\n\n        /**\n         * @event deferredBinding.newUnbindInstance\n         * Called after the DeferredBindingModel is initialized for unbinding\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('deferredBinding.newUnbindInstance', function ((\\Model) $model) {\n         *         $model->some_attribute = true;\n         *     });\n         *\n         */\n        if ($event = $this->fireEvent('deferredBinding.newUnbindInstance', $binding, true)) {\n            $binding = $event;\n        }\n\n        $binding->save();\n\n        return $binding;\n    }\n\n    /**\n     * cancelDeferred cancels all deferred bindings to this model\n     */\n    public function cancelDeferred($sessionKey): void\n    {\n        DeferredBindingModel::cancelDeferredActions(get_class($this), $sessionKey);\n    }\n\n    /**\n     * commitDeferred commits all deferred bindings to this model\n     */\n    public function commitDeferred($sessionKey)\n    {\n        $this->commitDeferredOfType($sessionKey);\n    }\n\n    /**\n     * commitDeferredBefore is used internally to commit all deferred bindings before saving.\n     * It is a rare need to have to call this, since it only applies to the\n     * \"belongs to\" relationship which generally does not need deferring.\n     */\n    protected function commitDeferredBefore($sessionKey)\n    {\n        $this->commitDeferredOfType($sessionKey, 'belongsTo');\n    }\n\n    /**\n     * commitDeferredAfter is used internally to commit all deferred bindings after saving\n     */\n    protected function commitDeferredAfter($sessionKey)\n    {\n        $this->commitDeferredOfType($sessionKey, null, 'belongsTo');\n    }\n\n    /**\n     * commitDeferredOfType is an internal method for committing deferred relations\n     */\n    protected function commitDeferredOfType($sessionKey, $include = null, $exclude = null)\n    {\n        if (!strlen($sessionKey)) {\n            return;\n        }\n\n        $bindings = $this->getDeferredBindingRecords($sessionKey);\n\n        foreach ($bindings as $binding) {\n            if (!($relationName = $binding->master_field)) {\n                continue;\n            }\n\n            if (!$this->hasRelation($relationName)) {\n                continue;\n            }\n\n            $relationType = $this->getRelationType($relationName);\n            $allowedTypes = $this->getDeferrableRelationTypes();\n\n            if ($include) {\n                $allowedTypes = array_intersect($allowedTypes, (array) $include);\n            }\n            elseif ($exclude) {\n                $allowedTypes = array_diff($allowedTypes, (array) $exclude);\n            }\n\n            if (!in_array($relationType, $allowedTypes)) {\n                continue;\n            }\n\n            // Find the slave model\n            $slaveClass = $binding->slave_type;\n            $slaveModel = $this->makeRelation($relationName);\n            if (!is_a($slaveModel, $slaveClass)) {\n                continue;\n            }\n\n            $slaveModel = $slaveModel->find($binding->slave_id);\n            if (!$slaveModel) {\n                continue;\n            }\n\n            // Bind/Unbind the relationship, save the related model with any\n            // deferred bindings it might have and delete the binding action\n            $relationObj = $this->$relationName();\n            if ($binding->is_bind) {\n                if (in_array($relationType, ['belongsToMany', 'morphToMany', 'morphedByMany'])) {\n                    $pivotData = $binding->getPivotDataForBind($this, $relationName);\n                    $relationObj->add($slaveModel, null, $pivotData);\n                }\n                else {\n                    $relationObj->add($slaveModel);\n                }\n            }\n            else {\n                $relationObj->remove($slaveModel);\n            }\n\n            $binding->delete();\n        }\n    }\n\n    /**\n     * getDeferredBindingRecords returns any outstanding binding records for this model\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    protected function getDeferredBindingRecords($sessionKey)\n    {\n        $binding = new DeferredBindingModel;\n\n        $binding->setConnection($this->getConnectionName());\n\n        return $binding\n            ->where('master_type', get_class($this))\n            ->where('session_key', $sessionKey)\n            ->get()\n        ;\n    }\n\n    /**\n     * getDeferrableRelationTypes returns all possible relation types that can be deferred\n     * @return array\n     */\n    protected function getDeferrableRelationTypes()\n    {\n        return [\n            'hasMany',\n            'hasOne',\n            'morphMany',\n            'morphToMany',\n            'morphedByMany',\n            'morphOne',\n            'attachMany',\n            'attachOne',\n            'belongsToMany',\n            'belongsTo'\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Database/Traits/Encryptable.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\nuse Crypt;\nuse Exception;\nuse Illuminate\\Support\\Arr;\n\n/**\n * Encryptable database trait\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait Encryptable\n{\n    /**\n     * @var array encryptable is a list of attribute names which should be encrypted\n     *\n     * protected $encryptable = [];\n     */\n\n    /**\n     * @var array originalEncryptableValues is the original attribute values\n     * before they were encrypted\n     */\n    protected $originalEncryptableValues = [];\n\n    /**\n     * initializeEncryptable trait for a model\n     */\n    public function initializeEncryptable()\n    {\n        if (!is_array($this->encryptable)) {\n            throw new Exception(sprintf(\n                'The $encryptable property in %s must be an array to use the Encryptable trait.',\n                static::class\n            ));\n        }\n\n        // Encrypt required fields when necessary\n        $this->bindEvent('model.beforeSetAttribute', function ($key, $value) {\n            if (\n                in_array($key, $this->getEncryptableAttributes()) &&\n                $value !== null &&\n                $value !== ''\n            ) {\n                return $this->makeEncryptableValue($key, $value);\n            }\n        });\n\n        $this->bindEvent('model.beforeGetAttribute', function ($key) {\n            if (\n                in_array($key, $this->getEncryptableAttributes()) &&\n                Arr::get($this->attributes, $key) !== null &&\n                Arr::get($this->attributes, $key) !== ''\n            ) {\n                return $this->getEncryptableValue($key);\n            }\n        });\n    }\n\n    /**\n     * makeEncryptableValue encrypts an attribute value and saves it in the original locker\n     * @param  string $key   Attribute\n     * @param  string $value Value to encrypt\n     * @return string Encrypted value\n     */\n    public function makeEncryptableValue($key, $value)\n    {\n        $this->originalEncryptableValues[$key] = $value;\n\n        return Crypt::encrypt($value);\n    }\n\n    /**\n     * getEncryptableValue decrypts an attribute value\n     * @param  string $key Attribute\n     * @return string Decrypted value\n     */\n    public function getEncryptableValue($key)\n    {\n        return Crypt::decrypt($this->attributes[$key]);\n    }\n\n    /**\n     * getEncryptableAttributes returns a collection of fields that will be encrypted.\n     * @return array\n     */\n    public function getEncryptableAttributes()\n    {\n        return $this->encryptable;\n    }\n\n    /**\n     * getOriginalEncryptableValues returns the original values of any encrypted attributes\n     * @return array\n     */\n    public function getOriginalEncryptableValues()\n    {\n        return $this->originalEncryptableValues;\n    }\n\n    /**\n     * getOriginalEncryptableValue returns the original values of any encrypted attributes.\n     * @return mixed\n     */\n    public function getOriginalEncryptableValue($attribute)\n    {\n        return $this->originalEncryptableValues[$attribute] ?? null;\n    }\n}\n"
  },
  {
    "path": "src/Database/Traits/Hashable.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\nuse Hash;\nuse Exception;\n\n/**\n * Hashable\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait Hashable\n{\n    /**\n     * @var array List of attribute names which should be hashed using the Bcrypt hashing algorithm.\n     *\n     * protected $hashable = [];\n     */\n\n    /**\n     * @var array List of original attribute values before they were hashed.\n     */\n    protected $originalHashableValues = [];\n\n    /**\n     * initializeHashable trait for a model.\n     */\n    public function initializeHashable()\n    {\n        if (!is_array($this->hashable)) {\n            throw new Exception(sprintf(\n                'The $hashable property in %s must be an array to use the Hashable trait.',\n                static::class\n            ));\n        }\n\n        // Hash required fields when necessary\n        $this->bindEvent('model.beforeSetAttribute', function ($key, $value) {\n            $hashable = $this->getHashableAttributes();\n            if (in_array($key, $hashable) && !empty($value)) {\n                return $this->makeHashValue($key, $value);\n            }\n        });\n    }\n\n    /**\n     * addHashable adds an attribute to the hashable attributes list\n     * @param  array|string|null  $attributes\n     * @return $this\n     */\n    public function addHashable($attributes = null)\n    {\n        $attributes = is_array($attributes) ? $attributes : func_get_args();\n\n        $this->hashable = array_merge($this->hashable, $attributes);\n\n        return $this;\n    }\n\n    /**\n     * makeHashValue hashes an attribute value and saves it in the original locker.\n     * @param  string $key   Attribute\n     * @param  string $value Value to hash\n     * @return string        Hashed value\n     */\n    public function makeHashValue($key, $value)\n    {\n        $this->originalHashableValues[$key] = $value;\n        return Hash::make($value);\n    }\n\n    /**\n     * checkHashValue checks if the supplied plain value matches the stored hash value.\n     * @param  string $key   Attribute to check\n     * @param  string $value Value to check\n     * @return bool\n     */\n    public function checkHashValue($key, $value)\n    {\n        return Hash::check($value, $this->{$key});\n    }\n\n    /**\n     * getHashableAttributes returns a collection of fields that will be hashed.\n     * @return array\n     */\n    public function getHashableAttributes()\n    {\n        return $this->hashable;\n    }\n\n    /**\n     * getOriginalHashValues returns the original values of any hashed attributes.\n     * @return array\n     */\n    public function getOriginalHashValues()\n    {\n        return $this->originalHashableValues;\n    }\n\n    /**\n     * getOriginalHashValue returns the original values of any hashed attributes.\n     * @return mixed\n     */\n    public function getOriginalHashValue($attribute)\n    {\n        return $this->originalHashableValues[$attribute] ?? null;\n    }\n}\n"
  },
  {
    "path": "src/Database/Traits/Multisite.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\nuse Illuminate\\Support\\Arr;\nuse Site;\nuse October\\Rain\\Database\\Scopes\\MultisiteScope;\nuse Exception;\n\n/**\n * Multisite trait allows for site-based models, the database\n * table should contain site_id and site_root_id keys\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait Multisite\n{\n    /**\n     * @var array propagatable list of attributes to propagate to other sites.\n     *\n     *     protected $propagatable = [];\n     */\n\n    /**\n     * @var bool|array propagatableSync will enforce model structures between all sites.\n     * When set to `false` will disable sync, set `true` will sync between the site group.\n     * The sync option allow sync to `all` sites, sites in the `group`, and sites the `locale`.\n     *\n     * Set to an array of options for more granular controls:\n     *\n     * - **sync** - logic to sync specific sites, available options: `all`, `group`, `locale`\n     * - **structure** - enable the sync of tree/sortable structures, default: `true`\n     * - **delete** - delete all linked records when any record is deleted, default: `true`\n     *\n     *     protected $propagatableSync = false;\n     */\n\n    /**\n     * bootMultisite trait for a model.\n     */\n    public static function bootMultisite()\n    {\n        static::addGlobalScope(new MultisiteScope);\n    }\n\n    /**\n     * @var bool multisiteRelationsDefined prevents redundant processing during\n     * unserialization (__wakeup) since relation definitions persist through serialization.\n     */\n    protected $multisiteRelationsDefined = false;\n\n    /**\n     * initializeMultisite\n     */\n    public function initializeMultisite()\n    {\n        if (!is_array($this->propagatable)) {\n            throw new Exception(sprintf(\n                'The $propagatable property in %s must be an array to use the Multisite trait.',\n                static::class\n            ));\n        }\n\n        $this->bindEvent('model.beforeSave', [$this, 'multisiteBeforeSave']);\n\n        $this->bindEvent('model.afterCreate', [$this, 'multisiteAfterCreate']);\n\n        $this->bindEvent('model.saveComplete', [$this, 'multisiteSaveComplete']);\n\n        $this->bindEvent('model.afterDelete', [$this, 'multisiteAfterDelete']);\n\n        if (!$this->multisiteRelationsDefined) {\n            $this->defineMultisiteRelations();\n            $this->multisiteRelationsDefined = true;\n        }\n    }\n\n    /**\n     * multisiteBeforeSave constructor event used internally\n     */\n    public function multisiteBeforeSave()\n    {\n        if (Site::hasGlobalContext()) {\n            return;\n        }\n\n        $this->{$this->getSiteIdColumn()} = Site::getSiteIdFromContext();\n    }\n\n    /**\n     * multisiteSaveComplete constructor event used internally\n     */\n    public function multisiteSaveComplete()\n    {\n        if ($this->getSaveOption('propagate') !== true) {\n            return;\n        }\n\n        if (!$this->isMultisiteEnabled()) {\n            return;\n        }\n\n        Site::withGlobalContext(function() {\n            $otherModels = $this->newOtherSiteQuery()->get();\n            $otherSites = $this->getMultisiteSyncSites();\n\n            // Propagate attributes to known records\n            if ($this->propagatable) {\n                foreach ($otherSites as $siteId) {\n                    if ($model = $otherModels->where('site_id', $siteId)->first()) {\n                        $this->propagateToSite($siteId, $model);\n                    }\n                }\n            }\n\n            // Sync non-existent records\n            if ($this->isMultisiteSyncEnabled()) {\n                $missingSites = array_diff($otherSites, $otherModels->pluck('site_id')->all());\n                foreach ($missingSites as $missingSite) {\n                    $this->propagateToSite($missingSite);\n                }\n            }\n        });\n    }\n\n    /**\n     * multisiteAfterCreate constructor event used internally\n     */\n    public function multisiteAfterCreate()\n    {\n        if ($this->site_root_id) {\n            return;\n        }\n\n        $this->site_root_id = $this->id;\n        $this->newQueryWithoutScopes()\n            ->where($this->getKeyName(), $this->id)\n            ->update(['site_root_id' => $this->site_root_id])\n        ;\n    }\n\n    /**\n     * multisiteAfterDelete\n     */\n    public function multisiteAfterDelete()\n    {\n        if (!$this->isMultisiteSyncEnabled() || !$this->getMultisiteConfig('delete', true)) {\n            return;\n        }\n\n        Site::withGlobalContext(function() {\n            foreach ($this->getMultisiteSyncSites() as $siteId) {\n                if (!$this->isModelUsingSameSite($siteId)) {\n                    $this->deleteForSite($siteId);\n                }\n            }\n        });\n    }\n\n    /**\n     * defineMultisiteRelations will spin over every relation and apply propagation config\n     */\n    protected function defineMultisiteRelations()\n    {\n        foreach ($this->getRelationDefinitions() as $type => $relations) {\n            foreach ($this->$type as $name => $definition) {\n                if ($this->isAttributePropagatable($name)) {\n                    $this->defineMultisiteRelation($name, $type);\n                }\n            }\n        }\n    }\n\n    /**\n     * canDeleteMultisiteRelation checks if a relation has the potential to be shared with\n     * the current model. If there are 2 or more records in existence, then this method\n     * will prevent the cascading deletion of relations.\n     *\n     * @see \\October\\Rain\\Database\\Concerns\\HasRelationships::performDeleteOnRelations\n     */\n    public function canDeleteMultisiteRelation($name, $type = null): bool\n    {\n        // Attribute is exclusive to parent model without propagation\n        if (!$this->isAttributePropagatable($name)) {\n            return true;\n        }\n\n        if ($type === null) {\n            $type = $this->getRelationType($name);\n        }\n\n        // Type is not supported by multisite\n        if (!in_array($type, ['belongsToMany', 'morphToMany', 'morphedByMany', 'belongsTo', 'hasOne', 'hasMany', 'attachOne', 'attachMany'])) {\n            return true;\n        }\n\n        // The current record counts for one so halt if we find more\n        return !($this->newOtherSiteQuery()->count() > 1);\n    }\n\n    /**\n     * defineMultisiteRelation will modify defined relations on this model so they share\n     * their association using the shared identifier (`site_root_id`). Only these relation\n     * types support relation sharing: `belongsToMany`, `morphToMany`, `morphedByMany`,\n     * `belongsTo`, `hasOne`, `hasMany`, `attachOne`, `attachMany`.\n     *\n     * For many-to-many relations where both parent AND related models use multisite\n     * (dual-multisite), the keys remain as default 'id' and propagation handles syncing\n     * the correct site-specific related records.\n     */\n    protected function defineMultisiteRelation($name, $type = null)\n    {\n        if ($type === null) {\n            $type = $this->getRelationType($name);\n        }\n\n        if ($type) {\n            if (!is_array($this->$type[$name])) {\n                $this->$type[$name] = (array) $this->$type[$name];\n            }\n\n            // Override the local key to the shared root identifier\n            if (in_array($type, ['belongsToMany', 'morphToMany', 'morphedByMany'])) {\n                // Check if related model also uses multisite (dual-multisite scenario)\n                // In dual-multisite, keys stay as default 'id' and pivotSiteScope handles filtering\n                $relatedIsMultisite = $this->isRelatedMultisite($name);\n                if ($relatedIsMultisite) {\n                    // Dual-multisite: pivot queries should be scoped by site_id\n                    $this->$type[$name]['pivotSiteScope'] = true;\n                }\n                else {\n                    // Single-multisite: use site_root_id to share relations across sites\n                    $this->$type[$name]['parentKey'] = 'site_root_id';\n                }\n            }\n            elseif (in_array($type, ['belongsTo', 'hasOne', 'hasMany'])) {\n                $this->$type[$name]['otherKey'] = 'site_root_id';\n            }\n            elseif (in_array($type, ['attachOne', 'attachMany'])) {\n                $this->$type[$name]['key'] = 'site_root_id';\n            }\n        }\n    }\n\n    /**\n     * savePropagate the model, including to other sites\n     * @return bool\n     */\n    public function savePropagate($options = null, $sessionKey = null)\n    {\n        return $this->saveInternal((array) $options + ['propagate' => true, 'sessionKey' => $sessionKey]);\n    }\n\n    /**\n     * addPropagatable attributes for the model.\n     * @param  array|string|null  $attributes\n     */\n    public function addPropagatable($attributes = null)\n    {\n        $attributes = is_array($attributes) ? $attributes : func_get_args();\n\n        $this->propagatable = array_merge($this->propagatable, $attributes);\n\n        foreach ($attributes as $attribute) {\n            $this->defineMultisiteRelation($attribute);\n        }\n    }\n\n    /**\n     * isAttributePropagatable\n     * @return bool\n     */\n    public function isAttributePropagatable($attribute)\n    {\n        return in_array($attribute, $this->propagatable);\n    }\n\n    /**\n     * propagateToSite will save propagated fields to other records\n     */\n    public function propagateToSite($siteId, $otherModel = null)\n    {\n        if ($this->isModelUsingSameSite($siteId)) {\n            return;\n        }\n\n        if ($otherModel === null) {\n            $otherModel = $this->findOtherSiteModel($siteId);\n        }\n\n        // Perform propagation for existing records\n        if ($otherModel->exists) {\n            foreach ($this->propagatable as $name) {\n                $relationType = $this->getRelationType($name);\n\n                // Propagate local key relation\n                if ($relationType === 'belongsTo') {\n                    $fkName = $this->$name()->getForeignKeyName();\n                    $otherModel->$fkName = $this->$fkName;\n                }\n                // Propagate local attribute (not a relation)\n                elseif (!$relationType) {\n                    $otherModel->$name = $this->$name;\n                }\n            }\n        }\n\n        $otherModel->save(['force' => true]);\n\n        // Propagate many-to-many relations after save since pivot\n        // records require the model to have an ID\n        foreach ($this->propagatable as $name) {\n            $relationType = $this->getRelationType($name);\n            if (in_array($relationType, ['belongsToMany', 'morphToMany', 'morphedByMany'])) {\n                $this->propagateManyToManyRelation($name, $siteId, $otherModel);\n            }\n        }\n\n        return $otherModel;\n    }\n\n    /**\n     * propagateManyToManyRelation propagates a many-to-many relation to another site.\n     * For dual-multisite (both models use multisite), this finds the corresponding\n     * related records in the target site and syncs them.\n     */\n    protected function propagateManyToManyRelation($name, $siteId, $otherModel)\n    {\n        $relation = $this->$name();\n        $relatedModel = $relation->getRelated();\n\n        // Check if related model uses multisite (dual-multisite scenario)\n        $relatedIsMultisite = $relatedModel->isClassInstanceOf(\\October\\Contracts\\Database\\MultisiteInterface::class)\n            && $relatedModel->isMultisiteEnabled();\n\n        if (!$relatedIsMultisite) {\n            return;\n        }\n\n        // Get related site_root_ids from current model's related records\n        $relatedRootIds = $relation->pluck('site_root_id')->all();\n\n        if (empty($relatedRootIds)) {\n            // Clear relations on target if source has none\n            Site::withContext($siteId, function() use ($otherModel, $name) {\n                $otherModel->$name()->sync([]);\n            });\n            return;\n        }\n\n        // Find target site's corresponding records by site_root_id\n        $targetIds = $relatedModel->newQueryWithoutScopes()\n            ->whereIn('site_root_id', $relatedRootIds)\n            ->where('site_id', $siteId)\n            ->pluck('id')\n            ->all();\n\n        // Sync on target model within site context\n        Site::withContext($siteId, function() use ($otherModel, $name, $targetIds) {\n            $otherModel->$name()->sync($targetIds);\n        });\n    }\n\n    /**\n     * getMultisiteKey returns the root key if multisite is used\n     */\n    public function getMultisiteKey()\n    {\n        if (!$this->isMultisiteEnabled()) {\n            return $this->getKey();\n        }\n\n        return $this->site_root_id ?: $this->getKey();\n    }\n\n    /**\n     * isMultisiteEnabled allows for programmatic toggling\n     * @return bool\n     */\n    public function isMultisiteEnabled()\n    {\n        return true;\n    }\n\n    /**\n     * isRelatedMultisite checks if a related model class uses multisite.\n     * This checks that multisite is enabled via the MultisiteInterface.\n     */\n    protected function isRelatedMultisite($name): bool\n    {\n        $relation = $this->getRelationDefinition($name);\n        $relatedClass = $relation[0] ?? null;\n\n        if (!$relatedClass || !class_exists($relatedClass)) {\n            return false;\n        }\n\n        $relatedModel = new $relatedClass;\n\n        return $relatedModel->isClassInstanceOf(\\October\\Contracts\\Database\\MultisiteInterface::class)\n            && $relatedModel->isMultisiteEnabled();\n    }\n\n    /**\n     * isMultisiteSyncEnabled\n     */\n    public function isMultisiteSyncEnabled()\n    {\n        if (!property_exists($this, 'propagatableSync')) {\n            return false;\n        }\n\n        if (is_array($this->propagatableSync)) {\n            return ($this->propagatableSync['sync'] ?? false) !== false;\n        }\n\n        return (bool) $this->propagatableSync;\n    }\n\n    /**\n     * getMultisiteConfig\n     */\n    public function getMultisiteConfig($key, $default = null)\n    {\n        if (!property_exists($this, 'propagatableSync') || !is_array($this->propagatableSync)) {\n            return $default;\n        }\n\n        return Arr::get($this->propagatableSync, $key, $default);\n    }\n\n    /**\n     * getMultisiteSyncSites\n     * @return array\n     */\n    public function getMultisiteSyncSites()\n    {\n        if ($this->getMultisiteConfig('sync') === 'all') {\n            return Site::listSiteIds();\n        }\n\n        $siteId = $this->{$this->getSiteIdColumn()} ?: null;\n\n        if ($this->getMultisiteConfig('sync') === 'locale') {\n            return Site::listSiteIdsInLocale($siteId);\n        }\n\n        return Site::listSiteIdsInGroup($siteId);\n    }\n\n    /**\n     * scopeApplyOtherSiteRoot is used to resolve a model using its ID or its root ID.\n     * For example, finding a model using attributes from another site, or finding\n     * all connected models for all sites.\n     *\n     * If the value is provided as a string, it must be the ID from the primary record,\n     * in other words: taken from `site_root_id` not from the `id` column.\n     *\n     * @param \\Illuminate\\Database\\Eloquent\\Builder $query\n     * @param string|\\Illuminate\\Database\\Eloquent\\Model $idOrModel\n     * @return \\Illuminate\\Database\\Eloquent\\Builder\n     */\n    public function scopeApplyOtherSiteRoot($query, $idOrModel)\n    {\n        if ($idOrModel instanceof \\Illuminate\\Database\\Eloquent\\Model) {\n            $idOrModel = $idOrModel->site_root_id ?: $idOrModel->id;\n        }\n\n        return $query->where(function($q) use ($idOrModel) {\n            $q->where('id', $idOrModel);\n            $q->orWhere('site_root_id', $idOrModel);\n        });\n    }\n\n    /**\n     * newOtherSiteQuery\n     */\n    public function newOtherSiteQuery()\n    {\n        return $this->newQueryWithoutScopes()->applyOtherSiteRoot($this);\n    }\n\n    /**\n     * findForSite will locate a record for a specific site.\n     */\n    public function findForSite($siteId = null)\n    {\n        return $this\n            ->newOtherSiteQuery()\n            ->where($this->getSiteIdColumn(), $siteId)\n            ->first();\n    }\n\n    /**\n     * findOrCreateForSite\n     */\n    public function findOrCreateForSite($siteId = null)\n    {\n        $otherModel = $this->findOtherSiteModel($siteId);\n\n        // Newly created model\n        if (!$otherModel->exists) {\n            $otherModel->save(['force' => true]);\n        }\n\n        // Restoring a trashed model\n        if (\n            $otherModel->isClassInstanceOf(\\October\\Contracts\\Database\\SoftDeleteInterface::class) &&\n            $otherModel->trashed()\n        ) {\n            $otherModel->restore();\n        }\n\n        return $otherModel;\n    }\n\n    /**\n     * findOtherSiteModel\n     */\n    protected function findOtherSiteModel($siteId = null)\n    {\n        if ($siteId === null) {\n            $siteId = Site::getSiteIdFromContext();\n        }\n\n        if ($this->isModelUsingSameSite($siteId)) {\n            return $this;\n        }\n\n        $otherModel = $this->findForSite($siteId);\n\n        // Replicate without save\n        if (!$otherModel) {\n            $otherModel = $this->replicateWithRelations($this->getMultisiteConfig('except'));\n            $otherModel->{$this->getSiteIdColumn()} = $siteId;\n            $otherModel->site_root_id = $this->site_root_id ?: $this->id;\n        }\n\n        return $otherModel;\n    }\n\n    /**\n     * deleteForSite runs the delete command on a model for another site, useful for cleaning\n     * up records for other sites when the parent is deleted.\n     */\n    public function deleteForSite($siteId = null)\n    {\n        $otherModel = $this->findForSite($siteId);\n        if (!$otherModel) {\n            return;\n        }\n\n        $useSoftDeletes = $this->isClassInstanceOf(\\October\\Contracts\\Database\\SoftDeleteInterface::class);\n        if ($useSoftDeletes && !$this->isSoftDelete()) {\n            static::withoutEvents(function() use ($otherModel) {\n                $otherModel->forceDelete();\n            });\n            return;\n        }\n\n        static::withoutEvents(function() use ($otherModel) {\n            $otherModel->delete();\n        });\n    }\n\n    /**\n     * isModelUsingSameSite\n     */\n    protected function isModelUsingSameSite($siteId = null)\n    {\n        return (int) $this->{$this->getSiteIdColumn()} === (int) $siteId;\n    }\n\n    /**\n     * getSiteIdColumn gets the name of the \"site id\" column.\n     * @return string\n     */\n    public function getSiteIdColumn()\n    {\n        return defined('static::SITE_ID') ? static::SITE_ID : 'site_id';\n    }\n\n    /**\n     * getQualifiedSiteIdColumn gets the fully qualified \"site id\" column.\n     * @return string\n     */\n    public function getQualifiedSiteIdColumn()\n    {\n        return $this->qualifyColumn($this->getSiteIdColumn());\n    }\n}\n"
  },
  {
    "path": "src/Database/Traits/MultisiteGroup.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\nuse Site;\nuse October\\Rain\\Database\\Scopes\\MultisiteGroupScope;\n\n/**\n * MultisiteGroup trait scopes records by site_group_id (tenant).\n *\n * Unlike the Multisite trait which creates duplicate rows per locale (site_id),\n * this trait scopes a single record to a site group. Language/translation is\n * handled separately by the Translate plugin via $translatable.\n *\n * The database table should contain a site_group_id column.\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait MultisiteGroup\n{\n    /**\n     * bootMultisiteGroup trait for a model.\n     */\n    public static function bootMultisiteGroup()\n    {\n        static::addGlobalScope(new MultisiteGroupScope);\n    }\n\n    /**\n     * initializeMultisiteGroup\n     */\n    public function initializeMultisiteGroup()\n    {\n        $this->bindEvent('model.beforeSave', [$this, 'multisiteGroupBeforeSave']);\n    }\n\n    /**\n     * multisiteGroupBeforeSave sets the site_group_id from context\n     */\n    public function multisiteGroupBeforeSave()\n    {\n        if (Site::hasGlobalContext()) {\n            return;\n        }\n\n        $this->{$this->getSiteGroupIdColumn()} = Site::getSiteGroupIdFromContext();\n    }\n\n    /**\n     * isMultisiteGroupEnabled allows for programmatic toggling\n     */\n    public function isMultisiteGroupEnabled(): bool\n    {\n        return true;\n    }\n\n    /**\n     * getSiteGroupIdColumn gets the name of the \"site group id\" column.\n     */\n    public function getSiteGroupIdColumn(): string\n    {\n        return defined('static::SITE_GROUP_ID') ? static::SITE_GROUP_ID : 'site_group_id';\n    }\n\n    /**\n     * getQualifiedSiteGroupIdColumn gets the fully qualified \"site group id\" column.\n     */\n    public function getQualifiedSiteGroupIdColumn(): string\n    {\n        return $this->qualifyColumn($this->getSiteGroupIdColumn());\n    }\n}\n"
  },
  {
    "path": "src/Database/Traits/NestedTree.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\nuse DbDongle;\nuse October\\Rain\\Database\\Collection;\nuse October\\Rain\\Database\\TreeCollection;\nuse October\\Rain\\Database\\Scopes\\NestedTreeScope;\nuse Exception;\n\n/**\n * NestedTree is a nested set model trait\n *\n * Model table must have parent_id, nest_left, nest_right and nest_depth table columns.\n * In the model class definition:\n *\n *   use \\October\\Rain\\Database\\Traits\\NestedTree;\n *\n *   $table->integer('parent_id')->nullable();\n *   $table->integer('nest_left')->nullable();\n *   $table->integer('nest_right')->nullable();\n *   $table->integer('nest_depth')->nullable();\n *\n * You can change the column names used by declaring:\n *\n *   const PARENT_ID = 'my_parent_column';\n *   const NEST_LEFT = 'my_left_column';\n *   const NEST_RIGHT = 'my_right_column';\n *   const NEST_DEPTH = 'my_depth_column';\n *\n * General access methods:\n *\n *   $model->getRoot(); // Returns the highest parent of a node.\n *   $model->getRootList(); // Returns an indented array of key and value columns from root.\n *   $model->getParent(); // The direct parent node.\n *   $model->getParents(); // Returns all parents up the tree.\n *   $model->getParentsAndSelf(); // Returns all parents up the tree and self.\n *   $model->getChildren(); // Set of all direct child nodes.\n *   $model->getSiblings(); // Return all siblings (parent's children).\n *   $model->getSiblingsAndSelf(); // Return all siblings and self.\n *   $model->getLeaves(); // Returns all final nodes without children.\n *   $model->getDepth(); // Returns the depth of a current node.\n *   $model->getChildCount(); // Returns number of all children.\n *\n * Query builder methods:\n *\n *   $query->withoutNode(); // Filters a specific node from the results.\n *   $query->withoutSelf(); // Filters current node from the results.\n *   $query->withoutRoot(); // Filters root from the results.\n *   $query->children(); // Filters as direct children down the tree.\n *   $query->allChildren(); // Filters as all children down the tree.\n *   $query->parent(); // Filters as direct parent up the tree.\n *   $query->parents(); // Filters as all parents up the tree.\n *   $query->siblings(); // Filters as all siblings (parent's children).\n *   $query->leaves(); // Filters as all final nodes without children.\n *   $query->getNested(); // Returns an eager loaded collection of results.\n *   $query->listsNested(); // Returns an indented array of key and value columns.\n *\n * Flat result access methods:\n *\n *   $model->getAll(); // Returns everything in correct order.\n *   $model->getAllRoot(); // Returns all root nodes.\n *   $model->getAllChildren(); // Returns all children down the tree.\n *   $model->getAllChildrenAndSelf(); // Returns all children and self.\n *\n * Eager loaded access methods:\n *\n *   $model->getEagerRoot(); // Returns a list of all root nodes, with ->children eager loaded.\n *   $model->getEagerChildren(); // Returns direct child nodes, with ->children eager loaded.\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait NestedTree\n{\n    /**\n     * @var int moveToNewParentId indicates if the model should be aligned to new parent.\n     */\n    protected $moveToNewParentId = false;\n\n    /**\n     * bootNestedTree constructor\n     */\n    public static function bootNestedTree()\n    {\n        static::addGlobalScope(new NestedTreeScope);\n    }\n\n    /**\n     * initializeNestedTree constructor\n     */\n    public function initializeNestedTree()\n    {\n        // Define relationships\n        $this->hasMany['children'] = [\n            static::class,\n            'key' => $this->getParentColumnName(),\n            'replicate' => false\n        ];\n\n        $this->belongsTo['parent'] = [\n            static::class,\n            'key' => $this->getParentColumnName(),\n            'replicate' => false\n        ];\n\n        // Bind events\n        $this->bindEvent('model.beforeCreate', function () {\n            $this->setDefaultLeftAndRight();\n        });\n\n        $this->bindEvent('model.beforeSave', function () {\n            $this->storeNewParent();\n        });\n\n        $this->bindEvent('model.afterSave', function () {\n            $this->moveToNewParent();\n        });\n\n        $this->bindEvent('model.beforeDelete', function () {\n            $this->deleteDescendants();\n        });\n\n        $this->bindEvent('model.beforeRestore', function () {\n            $this->shiftSiblingsForRestore();\n        });\n\n        $this->bindEvent('model.afterRestore', function () {\n            $this->restoreDescendants();\n        });\n    }\n\n    /**\n     * storeNewParent handles if the parent column is modified so it can be realigned.\n     */\n    public function storeNewParent()\n    {\n        // Has the parent column been set from the outside\n        $parentId = $this->getParentId();\n        $parentOld = $this->getOriginal($this->getParentColumnName());\n        $isDirty = (int) $parentOld !== (int) $parentId;\n\n        // The parent ID column is nullable, including zero values,\n        // skipping this logic if the parent ID is already null.\n        if (!$parentId && $parentId !== null) {\n            $this->setAttribute($this->getParentColumnName(), null);\n            $parentId = null;\n        }\n\n        // Parent is not set or unchanged\n        if (!$isDirty) {\n            $this->moveToNewParentId = false;\n        }\n        // Created as a root node\n        elseif (!$this->exists && !$parentId) {\n            $this->moveToNewParentId = false;\n        }\n        // Parent has been set\n        else {\n            $this->moveToNewParentId = $parentId;\n        }\n    }\n\n    /**\n     * moveToNewParent will realign the nesting if the parent identifier is dirty.\n     */\n    public function moveToNewParent()\n    {\n        $parentId = $this->moveToNewParentId;\n        if ($parentId === false) {\n            return;\n        }\n\n        if ($parentId === null) {\n            $this->makeRoot();\n            return;\n        }\n\n        $parentModel = $this->resolveMoveTarget($parentId);\n        if ($parentModel) {\n            $this->makeChildOf($parentModel);\n            return;\n        }\n\n        // Nullify parent since nothing valid was found\n        $this->newNestedTreeQuery()\n            ->where($this->getKeyName(), $this->getKey())\n            ->update([$this->getParentColumnName() => null]);\n    }\n\n    /**\n     * deleteDescendants deletes a branch off the tree, shifting all the elements on the right\n     * back to the left so the counts work.\n     */\n    public function deleteDescendants()\n    {\n        if ($this->getRight() === null || $this->getLeft() === null) {\n            return;\n        }\n\n        $this->getConnection()->transaction(function () {\n            $this->reload();\n\n            $leftCol = $this->getLeftColumnName();\n            $rightCol = $this->getRightColumnName();\n            $left = $this->getLeft();\n            $right = $this->getRight();\n\n            // Delete children\n            $this->newNestedTreeQuery()\n                ->where($leftCol, '>', $left)\n                ->where($rightCol, '<', $right)\n                ->delete()\n            ;\n\n            // Update left and right indexes for the remaining nodes\n            $diff = $right - $left + 1;\n\n            $this->newNestedTreeQuery()\n                ->where($leftCol, '>', $right)\n                ->decrement($leftCol, $diff)\n            ;\n\n            $this->newNestedTreeQuery()\n                ->where($rightCol, '>', $right)\n                ->decrement($rightCol, $diff)\n            ;\n        });\n    }\n\n    /**\n     * shiftSiblingsForRestore allocates a slot for the the current node between its siblings.\n     */\n    public function shiftSiblingsForRestore()\n    {\n        if ($this->getRight() === null || $this->getLeft() === null) {\n            return;\n        }\n\n        $this->getConnection()->transaction(function () {\n            $leftCol = $this->getLeftColumnName();\n            $rightCol = $this->getRightColumnName();\n            $left = $this->getLeft();\n            $right = $this->getRight();\n\n            // Update left and right indexes for the remaining nodes\n            $diff = $right - $left + 1;\n\n            $this->newNestedTreeQuery()\n                ->where($leftCol, '>=', $left)\n                ->increment($leftCol, $diff)\n            ;\n\n            $this->newNestedTreeQuery()\n                ->where($rightCol, '>=', $left)\n                ->increment($rightCol, $diff)\n            ;\n        });\n    }\n\n    /**\n     * restoreDescendants of the current node.\n     */\n    public function restoreDescendants()\n    {\n        if ($this->getRight() === null || $this->getLeft() === null) {\n            return;\n        }\n\n        $this->getConnection()->transaction(function () {\n            $this->newNestedTreeQuery()\n                ->withTrashed()\n                ->where($this->getLeftColumnName(), '>', $this->getLeft())\n                ->where($this->getRightColumnName(), '<', $this->getRight())\n                ->update([\n                    $this->getDeletedAtColumn() => null,\n                    $this->getUpdatedAtColumn() => $this->{$this->getUpdatedAtColumn()}\n                ])\n            ;\n        });\n    }\n\n    //\n    // Alignment\n    //\n\n    /**\n     * makeRoot makes this model a root node.\n     * @return \\October\\Rain\\Database\\Model\n     */\n    public function makeRoot()\n    {\n        return $this->moveAfter($this->getRoot());\n    }\n\n    /**\n     * makeChildOf makes model node a child of specified node.\n     * @return \\October\\Rain\\Database\\Model\n     */\n    public function makeChildOf($node)\n    {\n        return $this->moveTo($node, 'child');\n    }\n\n    /**\n     * moveLeft finds the left sibling and move to left of it.\n     * @return \\October\\Rain\\Database\\Model\n     */\n    public function moveLeft()\n    {\n        return $this->moveBefore($this->getLeftSibling());\n    }\n\n    /**\n     * moveRight finds the right sibling and move to the right of it.\n     * @return \\October\\Rain\\Database\\Model\n     */\n    public function moveRight()\n    {\n        return $this->moveAfter($this->getRightSibling());\n    }\n\n    /**\n     * moveBefore moves to the model to before (left) specified node.\n     * @return \\October\\Rain\\Database\\Model\n     */\n    public function moveBefore($node)\n    {\n        return $this->moveTo($node, 'left');\n    }\n\n    /**\n     * moveAfter moves to the model to after (right) a specified node.\n     * @return \\October\\Rain\\Database\\Model\n     */\n    public function moveAfter($node)\n    {\n        return $this->moveTo($node, 'right');\n    }\n\n    //\n    // Checkers\n    //\n\n    /**\n     * isRoot returns true if this is a root node.\n     */\n    public function isRoot(): bool\n    {\n        return $this->getParentId() === null;\n    }\n\n    /**\n     * isChild returns true if this is a child node.\n     */\n    public function isChild(): bool\n    {\n        return !$this->isRoot();\n    }\n\n    /**\n     * isLeaf returns true if this is a leaf node (end of a branch).\n     */\n    public function isLeaf(): bool\n    {\n        return $this->exists && ($this->getRight() - $this->getLeft() === 1);\n    }\n\n    /**\n     * isInsideSubtree checks if the supplied node is inside the subtree of this model.\n     * @param \\Model\n     */\n    public function isInsideSubtree($node): bool\n    {\n        return (\n            $this->getLeft() >= $node->getLeft() &&\n            $this->getLeft() <= $node->getRight() &&\n            $this->getRight() >= $node->getLeft() &&\n            $this->getRight() <= $node->getRight()\n        );\n    }\n\n    /**\n     * isDescendantOf returns true if node is a descendant.\n     * @param NestedSet\n     */\n    public function isDescendantOf($other): bool\n    {\n        return ($this->getLeft() > $other->getLeft() && $this->getLeft() < $other->getRight());\n    }\n\n    //\n    // Scopes\n    //\n\n    /**\n     * scopeWithoutNode extracts a certain node object from the current query expression.\n     * @return \\Illuminate\\Database\\Query\\Builder\n     */\n    public function scopeWithoutNode($query, $node)\n    {\n        return $query->where($node->getKeyName(), '!=', $node->getKey());\n    }\n\n    /**\n     * scopeWithoutSelf extracts current node (self) from current query expression.\n     * @return \\Illuminate\\Database\\Query\\Builder\n     */\n    public function scopeWithoutSelf($query)\n    {\n        return $this->scopeWithoutNode($query, $this);\n    }\n\n    /**\n     * scopeWithoutRoot extracts first root (from the current node context) from current\n     * query expression.\n     * @return \\Illuminate\\Database\\Query\\Builder\n     */\n    public function scopeWithoutRoot($query)\n    {\n        return $this->scopeWithoutNode($query, $this->getRoot());\n    }\n\n    /**\n     * scopeAllChildren sets of all children & nested children.\n     * @return \\Illuminate\\Database\\Query\\Builder\n     */\n    public function scopeAllChildren($query, $includeSelf = false)\n    {\n        $query\n            ->where($this->getLeftColumnName(), '>=', $this->getLeft())\n            ->where($this->getLeftColumnName(), '<', $this->getRight())\n        ;\n\n        return $includeSelf ? $query : $query->withoutSelf();\n    }\n\n    /**\n     * scopeParents returns a prepared query with all parents up the tree.\n     * @return \\Illuminate\\Database\\Eloquent\\Builder\n     */\n    public function scopeParents($query, $includeSelf = false)\n    {\n        $query\n            ->where($this->getLeftColumnName(), '<=', $this->getLeft())\n            ->where($this->getRightColumnName(), '>=', $this->getRight())\n        ;\n\n        return $includeSelf ? $query : $query->withoutSelf();\n    }\n\n    /**\n     * scopeSiblings filters targeting all children of the parent, except self.\n     * @return \\Illuminate\\Database\\Eloquent\\Builder\n     */\n    public function scopeSiblings($query, $includeSelf = false)\n    {\n        $query->where($this->getParentColumnName(), $this->getParentId());\n\n        return $includeSelf ? $query : $query->withoutSelf();\n    }\n\n    /**\n     * scopeLeaves returns all final nodes without children.\n     * @return \\Illuminate\\Database\\Query\\Builder\n     */\n    public function scopeLeaves($query)\n    {\n        $grammar = $this->getConnection()->getQueryGrammar();\n\n        $rightCol = $grammar->wrap($this->getQualifiedRightColumnName());\n        $leftCol = $grammar->wrap($this->getQualifiedLeftColumnName());\n\n        return $query\n            ->allChildren()\n            ->whereRaw($rightCol . ' - ' . $leftCol . ' = 1')\n        ;\n    }\n\n    /**\n     * scopeGetAllRoot returns a list of all root nodes, without eager loading.\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    public function scopeGetAllRoot($query)\n    {\n        return $query\n            ->where(function ($query) {\n                $query->whereNull($this->getParentColumnName());\n                $query->orWhere($this->getParentColumnName(), 0);\n            })\n            ->get()\n        ;\n    }\n\n    /**\n     * scopeGetNested is a non-chaining scope, returns an eager loaded hierarchy tree.\n     * Children are eager loaded inside the $model->children relation.\n     * @return Collection A collection\n     */\n    public function scopeGetNested($query)\n    {\n        return $query->get()->toNested();\n    }\n\n    /**\n     * scopeListsNested gets an array with values of a given column. Values are indented\n     * according to their depth.\n     * @param  string $column Array values\n     * @param  string $key    Array keys\n     * @param  string $indent Character to indent depth\n     * @return array\n     */\n    public function scopeListsNested($query, $column, $key = null, $indent = '&nbsp;&nbsp;&nbsp;')\n    {\n        $resultKeyName = $this->getKeyName();\n        $columns = [$this->getDepthColumnName(), $this->getParentColumnName(), $this->getKeyName(), $column];\n        if ($key !== null) {\n            $resultKeyName = $key;\n            $columns[] = $key;\n        }\n\n        $values = $parentIds = [];\n        $results = $query->orderBy($this->getLeftColumnName())->get($columns);\n        foreach ($results as $result) {\n            $parentId = $result->{$this->getParentColumnName()};\n            if ($parentId && !isset($parentIds[$parentId])) {\n                continue;\n            }\n\n            $parentIds[$result->{$this->getKeyName()}] = true;\n            $values[$result->{$resultKeyName}] = str_repeat(\n                $indent,\n                $result->{$this->getDepthColumnName()}\n            ) . $result->{$column};\n        }\n\n        return $values;\n    }\n\n    //\n    // Getters\n    //\n\n    /**\n     * getAll returns all nodes and children.\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    public function getAll($columns = ['*'])\n    {\n        return $this->newNestedTreeQuery()->get($columns);\n    }\n\n    /**\n     * getRoot returns the root node starting from the current node.\n     * @return \\October\\Rain\\Database\\Model\n     */\n    public function getRoot()\n    {\n        if ($this->exists) {\n            return $this->newNestedTreeQuery()->parents(true)\n                ->where(function ($query) {\n                    $query->whereNull($this->getParentColumnName());\n                    $query->orWhere($this->getParentColumnName(), 0);\n                })\n                ->first()\n            ;\n        }\n\n        $parentId = $this->getParentId();\n\n        if ($parentId !== null && ($currentParent = $this->newNestedTreeQuery()->find($parentId))) {\n            return $currentParent->getRoot();\n        }\n\n        return $this;\n    }\n\n    /**\n     * getEagerRoot returns a list of all root nodes, with children eager loaded.\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    public function getEagerRoot()\n    {\n        return $this->newNestedTreeQuery()->getNested();\n    }\n\n    /**\n     * getRootList returns an array column/key pair of all root nodes, with children eager loaded.\n     * @return array\n     */\n    public function getRootList($column, $key = null, $indent = '&nbsp;&nbsp;&nbsp;')\n    {\n        return $this->newNestedTreeQuery()->listsNested($column, $key, $indent);\n    }\n\n    /**\n     * getParent returns the direct parent node.\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    public function getParent()\n    {\n        return $this->parent()->get();\n    }\n\n    /**\n     * getParents returns all parents up the tree.\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    public function getParents()\n    {\n        return $this->newNestedTreeQuery()->parents()->get();\n    }\n\n    /**\n     * getParentsAndSelf returns all parents up the tree and self.\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    public function getParentsAndSelf()\n    {\n        return $this->newNestedTreeQuery()->parents(true)->get();\n    }\n\n    /**\n     * getChildren returns direct child nodes.\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    public function getChildren()\n    {\n        return $this->children;\n    }\n\n    /**\n     * getEagerChildren returns direct child nodes, with ->children eager loaded.\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    public function getEagerChildren()\n    {\n        return $this->newNestedTreeQuery()->allChildren()->getNested();\n    }\n\n    /**\n     * getAllChildren returns all children down the tree.\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    public function getAllChildren()\n    {\n        return $this->newNestedTreeQuery()->allChildren()->get();\n    }\n\n    /**\n     * getAllChildrenAndSelf returns all children and self.\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    public function getAllChildrenAndSelf()\n    {\n        return $this->newNestedTreeQuery()->allChildren(true)->get();\n    }\n\n    /**\n     * getSiblings returns all siblings (parent's children).\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    public function getSiblings()\n    {\n        return $this->newNestedTreeQuery()->siblings()->get();\n    }\n\n    /**\n     * getSiblingsAndSelf\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    public function getSiblingsAndSelf()\n    {\n        return $this->newNestedTreeQuery()->siblings(true)->get();\n    }\n\n    /**\n     * getLeftSibling\n     * @return \\October\\Rain\\Database\\Model\n     */\n    public function getLeftSibling()\n    {\n        return $this->siblings()->where($this->getRightColumnName(), $this->getLeft() - 1)->first();\n    }\n\n    /**\n     * getRightSibling\n     * @return \\October\\Rain\\Database\\Model\n     */\n    public function getRightSibling()\n    {\n        return $this->siblings()->where($this->getLeftColumnName(), $this->getRight() + 1)->first();\n    }\n\n    /**\n     * getLeaves returns all final nodes without children.\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    public function getLeaves()\n    {\n        return $this->newNestedTreeQuery()->leaves()->get();\n    }\n\n    /**\n     * getLevel returns the level of this node in the tree. Root level is 0.\n     * @return int\n     */\n    public function getLevel()\n    {\n        if ($this->getParentId() === null) {\n            return 0;\n        }\n\n        return $this->newNestedTreeQuery()->parents()->count();\n    }\n\n    /**\n     * getChildCount returns number of all children below it.\n     * @return int\n     */\n    public function getChildCount()\n    {\n        return ($this->getRight() - $this->getLeft() - 1) / 2;\n    }\n\n    //\n    // Setters\n    //\n\n    /**\n     * setDepth sets the depth attribute.\n     * @return \\October\\Rain\\Database\\Model\n     */\n    public function setDepth()\n    {\n        $this->getConnection()->transaction(function () {\n            $this->reload();\n\n            $level = $this->getLevel();\n\n            $this->newNestedTreeQuery()\n                ->where($this->getKeyName(), $this->getKey())\n                ->update([$this->getDepthColumnName() => $level])\n            ;\n\n            $this->setAttribute($this->getDepthColumnName(), $level);\n        });\n\n        return $this;\n    }\n\n    /**\n     * setDefaultLeftAndRight columns\n     * @return void\n     */\n    public function setDefaultLeftAndRight()\n    {\n        $highRight = $this\n            ->newNestedTreeQuery()\n            ->reorder()\n            ->orderBy($this->getRightColumnName(), 'desc')\n            ->limit(1)\n            ->first()\n        ;\n\n        $maxRight = 0;\n        if ($highRight !== null) {\n            $maxRight = $highRight->getRight();\n        }\n\n        $this->setAttribute($this->getLeftColumnName(), $maxRight + 1);\n        $this->setAttribute($this->getRightColumnName(), $maxRight + 2);\n        $this->setAttribute($this->getDepthColumnName(), 0);\n    }\n\n    /**\n     * resetTreeNesting can be used to repair corrupt or missing tree definitions,\n     * it will flatten and heal the necessary columns, all parent and child\n     * associations are retained.\n     */\n    public function resetTreeNesting()\n    {\n        $this->getConnection()->transaction(function () {\n            $buildFunc = function($items, &$nest, $level = 0) use (&$buildFunc) {\n                $items->each(function ($item) use (&$nest, $level, $buildFunc) {\n                    $item->setAttribute($this->getLeftColumnName(), $nest++);\n                    $item->setAttribute($this->getDepthColumnName(), $level);\n                    $buildFunc($item->getChildren(), $nest, $level + 1);\n                    $item->setAttribute($this->getRightColumnName(), $nest++);\n                    $item->save(['force' => true]);\n                });\n            };\n\n            $records = $this\n                ->newNestedTreeQuery()\n                ->whereNull($this->getParentColumnName())\n                ->get()\n            ;\n\n            $nest = 1;\n            $buildFunc($records, $nest);\n        });\n    }\n\n    /**\n     * resetTreeOrphans can be used to locate orphaned records, those that refer\n     * to a parent_id value where the associated record no longer exists, and\n     * promote them to be visible in the collection again, by setting the\n     * parent column to null.\n     */\n    public function resetTreeOrphans()\n    {\n        $orphanMap = [];\n        $recordMap = $this\n            ->newNestedTreeQuery()\n            ->pluck($this->getParentColumnName(), $this->getKeyName())\n            ->all();\n\n        foreach ($recordMap as $id => $parent) {\n            if ($parent && !array_key_exists($parent, $recordMap)) {\n                $orphanMap[] = $id;\n            }\n        }\n\n        if ($orphanMap) {\n            $this->newNestedTreeQuery()\n                ->whereIn($this->getKeyName(), $orphanMap)\n                ->update([$this->getParentColumnName() => null]);\n        }\n    }\n\n    //\n    // Moving\n    //\n\n    /**\n     * moveTo is a handler for all node alignments.\n     * @param mixed  $target\n     * @param string $position\n     * @return \\October\\Rain\\Database\\Model\n     */\n    protected function moveTo($target, $position)\n    {\n        // Validate target\n        if ($target instanceof \\October\\Rain\\Database\\Model) {\n            $target->reload();\n        }\n        else {\n            $target = $this->resolveMoveTarget($target);\n        }\n\n        // Validate move\n        if (!$this->validateMove($this, $target, $position)) {\n            return $this;\n        }\n\n        // Perform move\n        $this->getConnection()->transaction(function () use ($target, $position) {\n            $this->performMove($this, $target, $position);\n        });\n\n        // Reapply alignments\n        $target->reload();\n        $this->setDepth();\n\n        foreach ($this->newNestedTreeQuery()->allChildren()->get() as $descendant) {\n            $descendant->setDepth();\n        }\n\n        $this->reload();\n        return $this;\n    }\n\n    /**\n     * performMove executes the SQL query associated with the update of the indexes affected\n     * by the move operation.\n     * @return int\n     */\n    protected function performMove($node, $target, $position)\n    {\n        [$a, $b, $c, $d] = $this->getSortedBoundaries($node, $target, $position);\n\n        $connection = $node->getConnection();\n        $grammar = $connection->getQueryGrammar();\n        $pdo = $connection->getPdo();\n\n        $parentId = $position === 'child'\n            ? $target->getKey()\n            : $target->getParentId();\n\n        if ($parentId === null) {\n            $parentId = 'NULL';\n        }\n        else {\n            $parentId = $pdo->quote($parentId);\n        }\n\n        $currentId = $pdo->quote($node->getKey());\n        $leftColumn = $node->getLeftColumnName();\n        $rightColumn = $node->getRightColumnName();\n        $parentColumn = $node->getParentColumnName();\n        $wrappedLeft = $grammar->wrap($leftColumn);\n        $wrappedRight = $grammar->wrap($rightColumn);\n        $wrappedParent = $grammar->wrap($parentColumn);\n        $wrappedId = DbDongle::cast($grammar->wrap($node->getKeyName()), 'TEXT');\n\n        $leftSql = \"CASE\n            WHEN $wrappedLeft BETWEEN $a AND $b THEN $wrappedLeft + $d - $b\n            WHEN $wrappedLeft BETWEEN $c AND $d THEN $wrappedLeft + $a - $c\n            ELSE $wrappedLeft END\";\n\n        $rightSql = \"CASE\n            WHEN $wrappedRight BETWEEN $a AND $b THEN $wrappedRight + $d - $b\n            WHEN $wrappedRight BETWEEN $c AND $d THEN $wrappedRight + $a - $c\n            ELSE $wrappedRight END\";\n\n        $parentSql = \"CASE\n            WHEN $wrappedId = $currentId THEN $parentId\n            ELSE $wrappedParent END\";\n\n        $result = $node->newNestedTreeQuery()\n            ->where(function ($query) use ($leftColumn, $rightColumn, $a, $d) {\n                $query\n                    ->whereBetween($leftColumn, [$a, $d])\n                    ->orWhereBetween($rightColumn, [$a, $d])\n                ;\n            })\n            ->update([\n                $leftColumn => $connection->raw($leftSql),\n                $rightColumn => $connection->raw($rightSql),\n                $parentColumn => $connection->raw($parentSql)\n            ])\n        ;\n\n        return $result;\n    }\n\n    /**\n     * resolveMoveTarget\n     * @return \\October\\Rain\\Database\\Model|null\n     */\n    protected function resolveMoveTarget($targetId)\n    {\n        $query = $this->newNestedTreeQuery();\n\n        if (\n            $this->isClassInstanceOf(\\October\\Contracts\\Database\\MultisiteInterface::class) &&\n            $this->isMultisiteEnabled()\n        ) {\n            return $query->applyOtherSiteRoot($targetId)->first();\n        }\n\n        return $query->find($targetId);\n    }\n\n    /**\n     * validateMove validates a proposed move and returns true if changes are needed.\n     * @return void\n     */\n    protected function validateMove($node, $target, $position)\n    {\n        if (!$node->exists) {\n            throw new Exception('A new node cannot be moved.');\n        }\n\n        if (!in_array($position, ['child', 'left', 'right'])) {\n            throw new Exception(sprintf(\n                'Position should be either child, left, right. Supplied position is \"%s\".',\n                $position\n            ));\n        }\n\n        if ($target === null) {\n            if ($position === 'left' || $position === 'right') {\n                throw new Exception(sprintf(\n                    'Cannot resolve target node. This node cannot move any further to the %s.',\n                    $position\n                ));\n            }\n\n            throw new Exception('Cannot resolve target node.');\n        }\n\n        if ($node === $target) {\n            throw new Exception('A node cannot be moved to itself.');\n        }\n\n        if ($target->isInsideSubtree($node)) {\n            throw new Exception('A node cannot be moved to a descendant of itself.');\n        }\n\n        return !(\n            $this->getPrimaryBoundary($node, $target, $position) === $node->getRight() ||\n            $this->getPrimaryBoundary($node, $target, $position) === $node->getLeft()\n        );\n    }\n\n    /**\n     * getPrimaryBoundary calculates the boundary.\n     * @return int\n     */\n    protected function getPrimaryBoundary($node, $target, $position)\n    {\n        $primaryBoundary = null;\n        switch ($position) {\n            case 'child':\n                $primaryBoundary = $target->getRight();\n                break;\n\n            case 'left':\n                $primaryBoundary = $target->getLeft();\n                break;\n\n            case 'right':\n                $primaryBoundary = $target->getRight() + 1;\n                break;\n        }\n\n        return ($primaryBoundary > $node->getRight())\n            ? $primaryBoundary - 1\n            : $primaryBoundary;\n    }\n\n    /**\n     * getOtherBoundary calculates the other boundary.\n     * @return int\n     */\n    protected function getOtherBoundary($node, $target, $position)\n    {\n        return ($this->getPrimaryBoundary($node, $target, $position) > $node->getRight())\n            ? $node->getRight() + 1\n            : $node->getLeft() - 1;\n    }\n\n    /**\n     * getSortedBoundaries calculates a sorted boundaries array.\n     * @return array\n     */\n    protected function getSortedBoundaries($node, $target, $position)\n    {\n        $boundaries = [\n            $node->getLeft(),\n            $node->getRight(),\n            $this->getPrimaryBoundary($node, $target, $position),\n            $this->getOtherBoundary($node, $target, $position)\n        ];\n\n        sort($boundaries);\n\n        return $boundaries;\n    }\n\n    //\n    // Column getters\n    //\n\n    /**\n     * getParentColumnName\n     * @return string\n     */\n    public function getParentColumnName()\n    {\n        return defined('static::PARENT_ID') ? static::PARENT_ID : 'parent_id';\n    }\n\n    /**\n     * getQualifiedParentColumnName\n     * @return string\n     */\n    public function getQualifiedParentColumnName()\n    {\n        return $this->getTable(). '.' .$this->getParentColumnName();\n    }\n\n    /**\n     * getParentId gets value of the model parent_id column.\n     * @return int\n     */\n    public function getParentId()\n    {\n        return $this->getAttribute($this->getParentColumnName());\n    }\n\n    /**\n     * getLeftColumnName\n     * @return string\n     */\n    public function getLeftColumnName()\n    {\n        return defined('static::NEST_LEFT') ? static::NEST_LEFT : 'nest_left';\n    }\n\n    /**\n     * getQualifiedLeftColumnName\n     * @return string\n     */\n    public function getQualifiedLeftColumnName()\n    {\n        return $this->getTable() . '.' . $this->getLeftColumnName();\n    }\n\n    /**\n     * getLeft column value.\n     * @return int\n     */\n    public function getLeft()\n    {\n        return $this->getAttribute($this->getLeftColumnName());\n    }\n\n    /**\n     * getRightColumnName\n     * @return string\n     */\n    public function getRightColumnName()\n    {\n        return defined('static::NEST_RIGHT') ? static::NEST_RIGHT : 'nest_right';\n    }\n\n    /**\n     * getQualifiedRightColumnName\n     * @return string\n     */\n    public function getQualifiedRightColumnName()\n    {\n        return $this->getTable() . '.' . $this->getRightColumnName();\n    }\n\n    /**\n     * getRight column value.\n     * @return int\n     */\n    public function getRight()\n    {\n        return $this->getAttribute($this->getRightColumnName());\n    }\n\n    /**\n     * getDepthColumnName\n     * @return string\n     */\n    public function getDepthColumnName()\n    {\n        return defined('static::NEST_DEPTH') ? static::NEST_DEPTH : 'nest_depth';\n    }\n\n    /**\n     * getQualifiedDepthColumnName\n     * @return string\n     */\n    public function getQualifiedDepthColumnName()\n    {\n        return $this->getTable() . '.' . $this->getDepthColumnName();\n    }\n\n    /**\n     * getDepth column value.\n     * @return int\n     */\n    public function getDepth()\n    {\n        return $this->getAttribute($this->getDepthColumnName());\n    }\n\n    //\n    // Instances\n    //\n\n    /**\n     * newNestedTreeQuery creates a new query for nested sets\n     */\n    protected function newNestedTreeQuery()\n    {\n        return $this->newQuery();\n    }\n\n    /**\n     * newCollection returns a custom TreeCollection collection.\n     */\n    public function newCollection(array $models = [])\n    {\n        return new TreeCollection($models);\n    }\n}\n"
  },
  {
    "path": "src/Database/Traits/Nullable.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\nuse Exception;\n\n/**\n * Nullable will set empty attributes to values equivalent to NULL in the database.\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait Nullable\n{\n    /**\n     * @var array nullable attribute names which should be set to null when empty.\n     *\n     * protected $nullable = [];\n     */\n\n    /**\n     * initializeNullable trait for a model\n     */\n    public function initializeNullable()\n    {\n        if (!is_array($this->nullable)) {\n            throw new Exception(sprintf(\n                'The $nullable property in %s must be an array to use the Nullable trait.',\n                static::class\n            ));\n        }\n\n        $this->bindEvent('model.beforeSaveDone', [$this, 'nullableBeforeSave']);\n    }\n\n    /**\n     * addNullable attribute to the nullable attributes list\n     */\n    public function addNullable($attributes = null)\n    {\n        $attributes = is_array($attributes) ? $attributes : func_get_args();\n\n        $this->nullable = array_merge($this->nullable, $attributes);\n    }\n\n    /**\n     * checkNullableValue checks if the supplied value is empty, excluding zero.\n     */\n    public function checkNullableValue($value): bool\n    {\n        if ($value === 0 || $value === '0' || $value === 0.0 || $value === false) {\n            return false;\n        }\n\n        return empty($value);\n    }\n\n    /**\n     * nullableBeforeSave will nullify empty fields at time of saving.\n     */\n    public function nullableBeforeSave()\n    {\n        foreach ($this->nullable as $field) {\n            if ($this->checkNullableValue($this->{$field})) {\n                if ($this->exists) {\n                    $this->attributes[$field] = null;\n                }\n                else {\n                    unset($this->attributes[$field]);\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "src/Database/Traits/Purgeable.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\nuse Exception;\n\n/**\n * Purgeable\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait Purgeable\n{\n    /**\n     * @var array purgeable attribute names which should not be saved to the database.\n     *\n     * protected $purgeable = [];\n     */\n\n    /**\n     * @var array originalPurgeableValues before they were purged.\n     */\n    protected $originalPurgeableValues = [];\n\n    /**\n     * initializePurgeable trait for a model.\n     */\n    public function initializePurgeable()\n    {\n        if (!is_array($this->purgeable)) {\n            throw new Exception(sprintf(\n                'The $purgeable property in %s must be an array to use the Purgeable trait.',\n                static::class\n            ));\n        }\n\n        $this->bindEvent('model.beforeSaveDone', [$this, 'purgeAttributes']);\n    }\n\n    /**\n     * addPurgeable adds an attribute to the purgeable attributes list\n     * @param  array|string|null  $attributes\n     * @return void\n     */\n    public function addPurgeable($attributes = null)\n    {\n        $attributes = is_array($attributes) ? $attributes : func_get_args();\n\n        $this->purgeable = array_merge($this->purgeable, $attributes);\n    }\n\n    /**\n     * purgeAttributes removes purged attributes from the dataset, used before saving.\n     * Specify attributesToPurge, if unspecified, $purgeable property is used\n     * @param mixed $attributes\n     * @return array\n     */\n    public function purgeAttributes($attributesToPurge = null)\n    {\n        if ($attributesToPurge === null) {\n            $purgeable = $this->getPurgeableAttributes();\n        }\n        else {\n            $purgeable = (array) $attributesToPurge;\n        }\n\n        $attributes = $this->getAttributes();\n\n        $cleanAttributes = array_diff_key($attributes, array_flip($purgeable));\n\n        $originalAttributes = array_diff_key($attributes, $cleanAttributes);\n\n        $this->originalPurgeableValues = array_merge(\n            $this->originalPurgeableValues,\n            $originalAttributes\n        );\n\n        return $this->attributes = $cleanAttributes;\n    }\n\n    /**\n     * getPurgeableAttributes returns a collection of fields that will be hashed.\n     */\n    public function getPurgeableAttributes()\n    {\n        return $this->purgeable;\n    }\n\n    /**\n     * getOriginalPurgeValues returns the original values of any purged attributes.\n     */\n    public function getOriginalPurgeValues()\n    {\n        return $this->originalPurgeableValues;\n    }\n\n    /**\n     * getOriginalPurgeValue returns the original values of any purged attributes.\n     */\n    public function getOriginalPurgeValue($attribute)\n    {\n        return $this->attributes[$attribute]\n            ?? ($this->originalPurgeableValues[$attribute] ?? null);\n    }\n\n    /**\n     * restorePurgedValues restores the original values of any purged attributes.\n     */\n    public function restorePurgedValues()\n    {\n        $this->attributes = array_merge(\n            $this->getAttributes(),\n            $this->originalPurgeableValues\n        );\n    }\n}\n"
  },
  {
    "path": "src/Database/Traits/Revisionable.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\nuse Db;\nuse Exception;\nuse DateTime;\nuse Illuminate\\Database\\Eloquent\\Model as EloquentModel;\nuse Illuminate\\Support\\Arr;\n\n/**\n * Revisionable trait tracks changes to specific attributes\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait Revisionable\n{\n    /**\n     * @var array revisionable list of attributes to monitor for changes and store revisions for.\n     *\n     * protected $revisionable = [];\n     */\n\n    /**\n     * @var int revisionableLimit is the maximum number of revision records to keep.\n     *\n     * public $revisionableLimit = 500;\n     */\n\n    /**\n     * @var const REVISION_HISTORY changes the relation name used to store revisions.\n     *\n     * const REVISION_HISTORY = 'revision_history';\n     */\n\n    /**\n     * @var bool revisionsEnabled flag for arbitrarily disabling revision history.\n     */\n    public $revisionsEnabled = true;\n\n    /**\n     * initializeRevisionable trait for a model.\n     */\n    public function initializeRevisionable()\n    {\n        if (!is_array($this->revisionable)) {\n            throw new Exception(sprintf(\n                'The $revisionable property in %s must be an array to use the Revisionable trait.',\n                static::class\n            ));\n        }\n\n        $this->bindEvent('model.afterUpdate', function () {\n            $this->revisionableAfterUpdate();\n        });\n\n        $this->bindEvent('model.afterDelete', function () {\n            $this->revisionableAfterDelete();\n        });\n    }\n\n    /**\n     * revisionableAfterUpdate event\n     */\n    public function revisionableAfterUpdate()\n    {\n        if (!$this->revisionsEnabled) {\n            return;\n        }\n\n        $relation = $this->getRevisionHistoryName();\n        $relationObject = $this->{$relation}();\n        $revisionModel = $relationObject->getRelated();\n\n        $toSave = [];\n        $dirty = $this->getDirty();\n        foreach ($dirty as $attribute => $value) {\n            if (!in_array($attribute, $this->revisionable)) {\n                continue;\n            }\n\n            $toSave[] = [\n                'field' => $attribute,\n                'old_value' => Arr::get($this->original, $attribute),\n                'new_value' => $value,\n                'revisionable_type' => $relationObject->getMorphClass(),\n                'revisionable_id' => $this->getKey(),\n                'user_id' => $this->revisionableGetUser(),\n                'cast' => $this->revisionableGetCastType($attribute),\n                'created_at' => new DateTime,\n                'updated_at' => new DateTime\n            ];\n        }\n\n        // Nothing to do\n        if (!count($toSave)) {\n            return;\n        }\n\n        Db::table($revisionModel->getTable())->insert($toSave);\n        $this->revisionableCleanUp();\n    }\n\n    /**\n     * revisionableAfterDelete event\n     */\n    public function revisionableAfterDelete()\n    {\n        if (!$this->revisionsEnabled) {\n            return;\n        }\n\n        $softDeletes = in_array(\n            \\October\\Rain\\Database\\Traits\\SoftDelete::class,\n            class_uses_recursive(static::class)\n        );\n\n        if (!$softDeletes) {\n            return;\n        }\n\n        if (!in_array('deleted_at', $this->revisionable)) {\n            return;\n        }\n\n        $relation = $this->getRevisionHistoryName();\n        $relationObject = $this->{$relation}();\n        $revisionModel = $relationObject->getRelated();\n\n        $toSave = [\n            'field' => 'deleted_at',\n            'old_value' => null,\n            'new_value' => $this->deleted_at,\n            'revisionable_type' => $relationObject->getMorphClass(),\n            'revisionable_id' => $this->getKey(),\n            'user_id' => $this->revisionableGetUser(),\n            'created_at' => new DateTime,\n            'updated_at' => new DateTime\n        ];\n\n        Db::table($revisionModel->getTable())->insert($toSave);\n        $this->revisionableCleanUp();\n    }\n\n    /*\n     * revisionableCleanUp deletes revision records exceeding the limit.\n     */\n    protected function revisionableCleanUp()\n    {\n        $relation = $this->getRevisionHistoryName();\n        $relationObject = $this->{$relation}();\n        $revisionLimit = property_exists($this, 'revisionableLimit')\n            ? (int) $this->revisionableLimit\n            : 500;\n\n        $toDelete = $relationObject\n            ->orderBy('id', 'desc')\n            ->skip($revisionLimit)\n            ->limit(64)\n            ->get();\n\n        foreach ($toDelete as $record) {\n            $record->delete();\n        }\n    }\n\n    /**\n     * revisionableGetCastType\n     */\n    protected function revisionableGetCastType($attribute)\n    {\n        if (in_array($attribute, $this->getDates())) {\n            return 'date';\n        }\n\n        return null;\n    }\n\n    /**\n     * revisionableGetUser\n     */\n    protected function revisionableGetUser()\n    {\n        if (method_exists($this, 'getRevisionableUser')) {\n            $user = $this->getRevisionableUser();\n\n            return $user instanceof EloquentModel\n                ? $user->getKey()\n                : $user;\n        }\n\n        return null;\n    }\n\n    /**\n     * getRevisionHistoryName\n     * @return string\n     */\n    public function getRevisionHistoryName()\n    {\n        return defined('static::REVISION_HISTORY') ? static::REVISION_HISTORY : 'revision_history';\n    }\n}\n"
  },
  {
    "path": "src/Database/Traits/SimpleTree.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\nuse Illuminate\\Support\\Arr;\nuse October\\Rain\\Database\\Collection;\nuse October\\Rain\\Database\\TreeCollection;\nuse Exception;\n\n/**\n * SimpleTree model trait\n *\n * Simple tree implementation, for advanced implementation see:\n * October\\Rain\\Database\\Traits\\NestedTree\n *\n * SimpleTree is the bare minimum needed for tree functionality, the\n * methods defined here should be implemented by all \"tree\" traits.\n *\n * Usage:\n *\n * Model table must have parent_id table column.\n * In the model class definition:\n *\n *   use \\October\\Rain\\Database\\Traits\\SimpleTree;\n *\n * General access methods:\n *\n *   $model->getChildren(); // Returns children of this node\n *   $model->getChildCount(); // Returns number of all children.\n *   $model->getAllChildren(); // Returns all children of this node\n *   $model->getAllRoot(); // Returns all root level nodes (eager loaded)\n *   $model->getAll(); // Returns everything in correct order.\n *\n * Query builder methods:\n *\n *   $query->listsNested(); // Returns an indented array of key and value columns.\n *\n * You can change the sort field used by declaring:\n *\n *   const PARENT_ID = 'my_parent_column';\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait SimpleTree\n{\n    /**\n     * initializeSimpleTree constructor\n     */\n    public function initializeSimpleTree()\n    {\n        // Define relationships\n        $this->hasMany['children'] = [\n            static::class,\n            'key' => $this->getParentColumnName(),\n            'replicate' => false\n        ];\n\n        $this->belongsTo['parent'] = [\n            static::class,\n            'key' => $this->getParentColumnName(),\n            'replicate' => false\n        ];\n\n        // Multisite\n        if (\n            $this->isClassInstanceOf(\\October\\Contracts\\Database\\MultisiteInterface::class) &&\n            $this->isMultisiteSyncEnabled() &&\n            $this->getMultisiteConfig('structure', true)\n        ) {\n            $this->addPropagatable(['children', 'parent']);\n        }\n    }\n\n    /**\n     * getAll returns all nodes and children.\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    public function getAll()\n    {\n        $collection = [];\n        foreach ($this->getAllRoot() as $rootNode) {\n            $collection[] = $rootNode;\n            $collection = $collection + $rootNode->getAllChildren()->getDictionary();\n        }\n\n        return new Collection($collection);\n    }\n\n    /**\n     * getAllChildren gets a list of children records, with their children (recursive)\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    public function getAllChildren()\n    {\n        $result = [];\n        $children = $this->getChildren();\n\n        foreach ($children as $child) {\n            $result[] = $child;\n\n            $childResult = $child->getAllChildren();\n            foreach ($childResult as $subChild) {\n                $result[] = $subChild;\n            }\n        }\n\n        return new Collection($result);\n    }\n\n    /**\n     * getChildren returns direct child nodes.\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    public function getChildren()\n    {\n        return $this->children;\n    }\n\n    /**\n     * getChildCount returns number of all children below it.\n     * @return int\n     */\n    public function getChildCount()\n    {\n        return count($this->getAllChildren());\n    }\n\n    /**\n     * getParents returns an array of parents, this is a heavy query and can produce\n     * in multiple queries.\n     */\n    public function getParents()\n    {\n        $result = [];\n\n        $parent = $this;\n        $result[] = $parent;\n\n        while ($parent = $parent->parent) {\n            $result[] = $parent;\n        }\n\n        return array_reverse($result);\n    }\n\n    //\n    // Scopes\n    //\n\n    /**\n     * scopeGetAllRoot returns a list of all root nodes, without eager loading.\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    public function scopeGetAllRoot($query)\n    {\n        return $query->where($this->getParentColumnName(), null)->get();\n    }\n\n    /**\n     * scopeGetNested is a non-chaining scope, returns an eager loaded hierarchy tree.\n     * Children are eager loaded inside the $model->children relation.\n     * @return Collection A collection\n     */\n    public function scopeGetNested($query)\n    {\n        return $query->get()->toNested();\n    }\n\n    /**\n     * scopeListsNested gets an array with values of a given column. Values are indented\n     * according to their depth.\n     * @param  string $column Array values\n     * @param  string $key    Array keys\n     * @param  string $indent Character to indent depth\n     * @return array\n     */\n    public function scopeListsNested($query, $column, $key = null, $indent = '&nbsp;&nbsp;&nbsp;')\n    {\n        $idName = $this->getKeyName();\n        $parentName = $this->getParentColumnName();\n\n        $columns = [$idName, $parentName, $column];\n        if ($key !== null) {\n            $columns[] = $key;\n        }\n\n        $collection = $query->getQuery()->get($columns);\n\n        // Assign all child nodes to their parents\n        $pairMap = [];\n        $rootItems = [];\n        foreach ($collection as $record) {\n            if ($parentId = $record->{$parentName}) {\n                if (!isset($pairMap[$parentId])) {\n                    $pairMap[$parentId] = [];\n                }\n                $pairMap[$parentId][] = $record;\n            }\n            else {\n                $rootItems[] = $record;\n            }\n        }\n\n        // Recursive helper function\n        $buildCollection = function(\n            $items,\n            $map,\n            $depth = 0\n        ) use (\n            &$buildCollection,\n            $column,\n            $key,\n            $indent,\n            $idName\n        ) {\n            $result = [];\n\n            $indentString = str_repeat($indent, $depth);\n\n            foreach ($items as $item) {\n                if (!property_exists($item, $column)) {\n                    throw new Exception('Column mismatch in listsNested method. Are you sure the columns exist?');\n                }\n\n                if ($key !== null) {\n                    $result[$item->{$key}] = $indentString . $item->{$column};\n                }\n                else {\n                    $result[] = $indentString . $item->{$column};\n                }\n\n                // Add the children\n                $childItems = Arr::get($map, $item->{$idName}, []);\n                if (count($childItems) > 0) {\n                    $result = $result + $buildCollection($childItems, $map, $depth + 1);\n                }\n            }\n\n            return $result;\n        };\n\n        // Build a nested collection\n        return $buildCollection($rootItems, $pairMap);\n    }\n\n    /**\n     * getParentColumnName\n     * @return string\n     */\n    public function getParentColumnName()\n    {\n        return defined('static::PARENT_ID') ? static::PARENT_ID : 'parent_id';\n    }\n\n    /**\n     * getQualifiedParentColumnName\n     * @return string\n     */\n    public function getQualifiedParentColumnName()\n    {\n        return $this->getTable(). '.' .$this->getParentColumnName();\n    }\n\n    /**\n     * getParentId gets value of the model parent_id column.\n     * @return int\n     */\n    public function getParentId()\n    {\n        return $this->getAttribute($this->getParentColumnName());\n    }\n\n    /**\n     * newCollection returns a custom TreeCollection collection.\n     */\n    public function newCollection(array $models = [])\n    {\n        return new TreeCollection($models);\n    }\n}\n"
  },
  {
    "path": "src/Database/Traits/Sluggable.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\nuse App;\nuse October\\Rain\\Support\\Str;\nuse Exception;\n\n/**\n * Sluggable trait performs automatic slug generation for new models\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait Sluggable\n{\n    /**\n     * @var array slugs are attributes to automatically generate unique URL names (slugs) for.\n     *\n     * protected $slugs = [];\n     */\n\n    /**\n     * initializeSluggable trait for a model.\n     */\n    public function initializeSluggable()\n    {\n        if (!is_array($this->slugs)) {\n            throw new Exception(sprintf(\n                'The $slugs property in %s must be an array to use the Sluggable trait.',\n                static::class\n            ));\n        }\n\n        // Set slugged attributes on new records and existing records if slug is missing.\n        $this->bindEvent('model.saveInternal', function () {\n            $this->slugAttributes();\n        });\n    }\n\n    /**\n     * slugAttributes adds slug attributes to the dataset, used before saving.\n     * @return void\n     */\n    public function slugAttributes()\n    {\n        foreach ($this->slugs as $slugAttribute => $sourceAttributes) {\n            $this->setSluggedValue($slugAttribute, $sourceAttributes);\n        }\n    }\n\n    /**\n     * setSluggedValue sets a single slug attribute value, using source attributes\n     * to generate the slug from and a maximum length for the slug not including\n     * the counter. Source attributes support dotted notation for relations.\n     * @param string $slugAttribute\n     * @param mixed $sourceAttributes\n     * @param int $maxLength\n     * @return string\n     */\n    public function setSluggedValue($slugAttribute, $sourceAttributes, $maxLength = 175)\n    {\n        if (!array_key_exists($slugAttribute, $this->attributes) || !mb_strlen($this->attributes[$slugAttribute])) {\n            if (!is_array($sourceAttributes)) {\n                $sourceAttributes = [$sourceAttributes];\n            }\n\n            $slugArr = [];\n            foreach ($sourceAttributes as $attribute) {\n                $slugArr[] = $this->getSluggableSourceAttributeValue($attribute);\n            }\n\n            $slug = implode(' ', $slugArr);\n            $slug = mb_substr($slug, 0, $maxLength);\n            $slug = Str::slug($slug, $this->getSluggableSeparator(), App::getLocale());\n        }\n        else {\n            $slug = $this->attributes[$slugAttribute] ?? '';\n        }\n\n        // Source attributes contain empty values, nothing to slug and this\n        // happens when the attributes are not required by the validator\n        if (!mb_strlen(trim($slug))) {\n            return $this->attributes[$slugAttribute] = '';\n        }\n\n        return $this->attributes[$slugAttribute] = $this->getSluggableUniqueAttributeValue($slugAttribute, $slug);\n    }\n\n    /**\n     * getSluggableUniqueAttributeValue ensures a unique attribute value, if the value is already\n     * used a counter suffix is added. Returns a safe value that is unique.\n     * @param string $name\n     * @param mixed $value\n     * @return string\n     */\n    protected function getSluggableUniqueAttributeValue($name, $value)\n    {\n        $counter = 1;\n        $separator = $this->getSluggableSeparator();\n        $_value = $value;\n\n        while ($this->newSluggableQuery()->where($name, $_value)->count() > 0) {\n            $counter++;\n            $_value = $value . $separator . $counter;\n        }\n\n        return $_value;\n    }\n\n    /**\n     * getSluggableSourceAttributeValue using dotted notation.\n     * Eg: author.name\n     * @return mixed\n     */\n    protected function getSluggableSourceAttributeValue($key)\n    {\n        if (strpos($key, '.') === false) {\n            return $this->getAttribute($key);\n        }\n\n        $keyParts = explode('.', $key);\n        $value = $this;\n        foreach ($keyParts as $part) {\n            if (!isset($value[$part])) {\n                return null;\n            }\n\n            $value = $value[$part];\n        }\n\n        return $value;\n    }\n\n    /**\n     * getSluggableSeparator is an override for the default slug separator.\n     * @return string\n     */\n    public function getSluggableSeparator()\n    {\n        return defined('static::SLUG_SEPARATOR') ? static::SLUG_SEPARATOR : '-';\n    }\n\n    /**\n     * newSluggableQuery returns a query that excludes the current record if it exists\n     * @return \\Illuminate\\Database\\Eloquent\\Builder\n     */\n    protected function newSluggableQuery()\n    {\n        $query = $this->newQuery();\n\n        if ($this->exists) {\n            $query->where($this->getKeyName(), '<>', $this->getKey());\n        }\n\n        if ($this->isClassInstanceOf(\\October\\Contracts\\Database\\SoftDeleteInterface::class)) {\n            $query->withTrashed();\n        }\n\n        if (\n            $this->isClassInstanceOf(\\October\\Contracts\\Database\\MultisiteInterface::class) &&\n            $this->isMultisiteEnabled()\n        ) {\n            $query->withSite($this->{$this->getSiteIdColumn()});\n        }\n\n        return $query;\n    }\n}\n"
  },
  {
    "path": "src/Database/Traits/SluggableTree.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\n/**\n * SluggableTree trait creates structured slugs, called full slugs. Calculating full slugs\n * must be performed externally since it involves expensive lookups. The model is assumed\n * to have two relations defined: parent, children.\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait SluggableTree\n{\n    /**\n     * fullSlugAttributes calculates full slugs for this model and all descendants\n     * @return void\n     */\n    public function fullSlugAttributes()\n    {\n        $this->setFullSluggedValue($this);\n    }\n\n    /**\n     * setFullSluggedValue will set the fullslug value on a model and recurse\n     * into children. For translatable models, the Translatable trait intercepts\n     * attribute access for the active locale automatically.\n     */\n    protected function setFullSluggedValue($model)\n    {\n        $fullslugAttr = $this->getFullSluggableFullSlugColumnName();\n        $proposedSlug = $this->getFullSluggableAttributeValue($model);\n\n        if ($model->{$fullslugAttr} !== $proposedSlug) {\n            $model->{$fullslugAttr} = $proposedSlug;\n            $model->saveQuietly(['force' => true]);\n        }\n\n        if ($children = $model->children) {\n            foreach ($children as $child) {\n                $this->setFullSluggedValue($child);\n            }\n        }\n    }\n\n    /**\n     * getFullSluggableAttributeValue builds the fullslug by walking up the\n     * parent chain using the model's slug attribute\n     */\n    protected function getFullSluggableAttributeValue($model, $fullslug = '')\n    {\n        $slugAttr = $this->getFullSluggableSlugColumnName();\n        $fullslug = $model->{$slugAttr} . '/' . $fullslug;\n\n        if ($parent = $model->parent()->withoutGlobalScopes()->first()) {\n            $fullslug = $this->getFullSluggableAttributeValue($parent, $fullslug);\n        }\n\n        return rtrim($fullslug, '/');\n    }\n\n    /**\n     * getFullSluggableFullSlugColumnName gets the name of the \"fullslug\" column.\n     * @return string\n     */\n    public function getFullSluggableFullSlugColumnName()\n    {\n        return defined('static::FULLSLUG') ? static::FULLSLUG : 'fullslug';\n    }\n\n    /**\n     * getFullSluggableSlugColumnName gets the name of the \"slug\" column.\n     * @return string\n     */\n    public function getFullSluggableSlugColumnName()\n    {\n        return defined('static::SLUG') ? static::SLUG : 'slug';\n    }\n}\n"
  },
  {
    "path": "src/Database/Traits/SoftDelete.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\nuse Illuminate\\Database\\Eloquent\\Model as EloquentModel;\nuse Illuminate\\Database\\Eloquent\\Collection as CollectionBase;\nuse Illuminate\\Support\\Arr;\nuse October\\Rain\\Database\\Scopes\\SoftDeleteScope;\n\n/**\n * SoftDelete trait for flagging models as deleted instead of actually deleting them.\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait SoftDelete\n{\n    /**\n     * @var bool forceDeleting indicates if the model is currently force deleting.\n     */\n    protected $forceDeleting = false;\n\n    /**\n     * bootSoftDelete trait for a model.\n     */\n    public static function bootSoftDelete()\n    {\n        static::addGlobalScope(new SoftDeleteScope);\n\n        static::softDeleted(function($model) {\n            /**\n             * @event model.afterTrash\n             * Called after the model is soft deleted (trashed)\n             *\n             * Example usage:\n             *\n             *     $model->bindEvent('model.afterTrash', function() use (\\October\\Rain\\Database\\Model $model) {\n             *         \\Log::info(\"{$model->name} has been trashed!\");\n             *     });\n             *\n             */\n            $model->fireEvent('model.afterTrash');\n            if ($model->methodExists('afterTrash')) {\n                $model->afterTrash();\n            }\n        });\n\n        static::restoring(function($model) {\n            /**\n             * @event model.beforeRestore\n             * Called before the model is restored from a soft delete\n             *\n             * Example usage:\n             *\n             *     $model->bindEvent('model.beforeRestore', function() use (\\October\\Rain\\Database\\Model $model) {\n             *         \\Log::info(\"{$model->name} is going to be restored!\");\n             *     });\n             *\n             */\n            $model->fireEvent('model.beforeRestore');\n            if ($model->methodExists('beforeRestore')) {\n                $model->beforeRestore();\n            }\n        });\n\n        static::restored(function($model) {\n            /**\n             * @event model.afterRestore\n             * Called after the model is restored from a soft delete\n             *\n             * Example usage:\n             *\n             *     $model->bindEvent('model.afterRestore', function() use (\\October\\Rain\\Database\\Model $model) {\n             *         \\Log::info(\"{$model->name} has been brought back to life!\");\n             *     });\n             *\n             */\n            $model->fireEvent('model.afterRestore');\n            if ($model->methodExists('afterRestore')) {\n                $model->afterRestore();\n            }\n        });\n    }\n\n    /**\n     * isSoftDelete helper method to check if the model is currently\n     * being hard or soft deleted, useful in events.\n     * @return bool\n     */\n    public function isSoftDelete()\n    {\n        return !$this->forceDeleting;\n    }\n\n    /**\n     * forceDelete on a soft deleted model.\n     */\n    public function forceDelete()\n    {\n        $this->forceDeleting = true;\n\n        $this->delete();\n\n        $this->forceDeleting = false;\n    }\n\n    /**\n     * performDeleteOnModel performs the actual delete query on this model instance.\n     */\n    protected function performDeleteOnModel()\n    {\n        if ($this->forceDeleting || !$this->isSoftDeleteEnabled()) {\n            $this->performDeleteOnRelations();\n\n            $this->setKeysForSaveQuery($this->newQuery()->withTrashed())->forceDelete();\n\n            $this->exists = false;\n        }\n        else {\n            $this->performSoftDeleteOnRelations();\n\n            $this->runSoftDelete();\n        }\n    }\n\n    /**\n     * performSoftDeleteOnRelations locates relations with softDelete flag and\n     * cascades the delete event.\n     */\n    protected function performSoftDeleteOnRelations()\n    {\n        $definitions = $this->getRelationDefinitions();\n        foreach ($definitions as $type => $relations) {\n            foreach ($relations as $name => $options) {\n                if (!Arr::get($options, 'softDelete', false)) {\n                    continue;\n                }\n\n                if (!$relation = $this->{$name}) {\n                    continue;\n                }\n\n                if ($relation instanceof EloquentModel) {\n                    $relation->delete();\n                }\n                elseif ($relation instanceof CollectionBase) {\n                    $relation->each(function ($model) {\n                        $model->delete();\n                    });\n                }\n            }\n        }\n    }\n\n    /**\n     * runSoftDelete performs the actual delete query on this model instance.\n     */\n    protected function runSoftDelete()\n    {\n        $query = $this->setKeysForSaveQuery($this->newQuery());\n\n        $time = $this->freshTimestamp();\n\n        $columns = [$this->getDeletedAtColumn() => $this->fromDateTime($time)];\n\n        $this->{$this->getDeletedAtColumn()} = $time;\n\n        if ($this->timestamps && ! is_null($this->getUpdatedAtColumn())) {\n            $this->{$this->getUpdatedAtColumn()} = $time;\n\n            $columns[$this->getUpdatedAtColumn()] = $this->fromDateTime($time);\n        }\n\n        $query->update($columns);\n\n        $this->syncOriginalAttributes(array_keys($columns));\n\n        $this->fireModelEvent('trashed', false);\n    }\n\n    /**\n     * restore a soft-deleted model instance.\n     * @return bool|null\n     */\n    public function restore()\n    {\n        // If the restoring event does not return false, we will proceed with this\n        // restore operation. Otherwise, we bail out so the developer will stop\n        // the restore totally. We will clear the deleted timestamp and save.\n        if ($this->fireModelEvent('restoring') === false) {\n            return false;\n        }\n\n        $this->performRestoreOnRelations();\n\n        $this->{$this->getDeletedAtColumn()} = null;\n\n        // Once we have saved the model, we will fire the \"restored\" event so this\n        // developer will do anything they need to after a restore operation is\n        // totally finished. Then we will return the result of the save call.\n        $this->exists = true;\n\n        $result = $this->save();\n\n        $this->fireModelEvent('restored', false);\n\n        return $result;\n    }\n\n    /**\n     * performRestoreOnRelations locates relations with softDelete flag and cascades\n     * the restore event.\n     */\n    protected function performRestoreOnRelations()\n    {\n        $definitions = $this->getRelationDefinitions();\n        foreach ($definitions as $type => $relations) {\n            foreach ($relations as $name => $options) {\n                if (!Arr::get($options, 'softDelete', false)) {\n                    continue;\n                }\n\n                $relation = $this->{$name}()->onlyTrashed()->getResults();\n                if (!$relation) {\n                    continue;\n                }\n\n                if ($relation instanceof EloquentModel) {\n                    $relation->restore();\n                }\n                elseif ($relation instanceof CollectionBase) {\n                    $relation->each(function ($model) {\n                        $model->restore();\n                    });\n                }\n            }\n        }\n    }\n\n    /**\n     * trashed determines if the model instance has been soft-deleted.\n     * @return bool\n     */\n    public function trashed()\n    {\n        return !is_null($this->{$this->getDeletedAtColumn()});\n    }\n\n    /**\n     * withTrashed gets a new query builder that includes soft deletes.\n     * @return \\Illuminate\\Database\\Eloquent\\Builder|static\n     */\n    public static function withTrashed()\n    {\n        return with(new static)->newQueryWithoutScope(new SoftDeleteScope);\n    }\n\n    /**\n     * onlyTrashed gets a new query builder that only includes soft deletes.\n     * @return \\Illuminate\\Database\\Eloquent\\Builder|static\n     */\n    public static function onlyTrashed()\n    {\n        $instance = new static;\n\n        $column = $instance->getQualifiedDeletedAtColumn();\n\n        return $instance->newQueryWithoutScope(new SoftDeleteScope)->whereNotNull($column);\n    }\n\n    /**\n     * softDeleted registers a \"trashed\" model event callback with the dispatcher.\n     * @param  \\Closure|string  $callback\n     * @return void\n     */\n    public static function softDeleted($callback)\n    {\n        static::registerModelEvent('trashed', $callback);\n    }\n\n    /**\n     * restoring registers a restoring model event with the dispatcher.\n     * @param  \\Closure|string  $callback\n     * @return void\n     */\n    public static function restoring($callback)\n    {\n        static::registerModelEvent('restoring', $callback);\n    }\n\n    /**\n     * restored registers a restored model event with the dispatcher.\n     * @param  \\Closure|string  $callback\n     * @return void\n     */\n    public static function restored($callback)\n    {\n        static::registerModelEvent('restored', $callback);\n    }\n\n    /**\n     * isSoftDeleteEnabled allows for programmatic toggling\n     * @return bool\n     */\n    public function isSoftDeleteEnabled()\n    {\n        return true;\n    }\n\n    /**\n     * getDeletedAtColumn gets the name of the \"deleted at\" column.\n     * @return string\n     */\n    public function getDeletedAtColumn()\n    {\n        return defined('static::DELETED_AT') ? static::DELETED_AT : 'deleted_at';\n    }\n\n    /**\n     * getQualifiedDeletedAtColumn gets the fully qualified \"deleted at\" column.\n     * @return string\n     */\n    public function getQualifiedDeletedAtColumn()\n    {\n        return $this->qualifyColumn($this->getDeletedAtColumn());\n    }\n}\n"
  },
  {
    "path": "src/Database/Traits/Sortable.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\nuse October\\Rain\\Database\\Scopes\\SortableScope;\nuse Exception;\n\n/**\n * Sortable model trait\n *\n * Usage:\n *\n * Model table must have sort_order table column.\n *\n * In the model class definition:\n *\n *   use \\October\\Rain\\Database\\Traits\\Sortable;\n *\n * To set orders:\n *\n *   $model->setSortableOrder($recordIds, $recordOrders);\n *\n * You can change the sort field used by declaring:\n *\n *   const SORT_ORDER = 'my_sort_order';\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait Sortable\n{\n    /**\n     * bootSortable trait for this model.\n     */\n    public static function bootSortable()\n    {\n        static::addGlobalScope(new SortableScope);\n    }\n\n    /**\n     * initializeSortable trait for this model.\n     */\n    public function initializeSortable()\n    {\n        $this->bindEvent('model.afterCreate', function () {\n            $sortOrderColumn = $this->getSortOrderColumn();\n\n            if (is_null($this->$sortOrderColumn)) {\n                $this->setSortableOrder([$this->getKey()], [$this->getKey()]);\n            }\n        });\n    }\n\n    /**\n     * setSortableOrder sets the sort order of records to the specified orders, supplying\n     * a reference pool of sorted values. If reference pool is true, then an incrementing\n     * pool is used.\n     * @param  mixed $itemIds\n     * @param  array|null|bool $referencePool\n     * @return void\n     */\n    public function setSortableOrder($itemIds, $referencePool = null)\n    {\n        if (!is_array($itemIds)) {\n            return;\n        }\n\n        $sortKeyMap = $this->processSortableOrdersInternal($itemIds, $referencePool);\n        if (count($itemIds) !== count($sortKeyMap)) {\n            throw new Exception('Invalid setSortableOrder call - count of itemIds do not match count of referencePool');\n        }\n\n        // Multisite\n        $keyName = $this->getKeyName();\n        if (\n            $this->isClassInstanceOf(\\October\\Contracts\\Database\\MultisiteInterface::class) &&\n            $this->isMultisiteSyncEnabled() &&\n            $this->getMultisiteConfig('structure', true)\n        ) {\n            $keyName = 'site_root_id';\n        }\n\n        $upsert = [];\n        foreach ($itemIds as $id) {\n            $sortOrder = $sortKeyMap[$id] ?? null;\n            if ($sortOrder !== null) {\n                $upsert[] = ['id' => $id, 'sort_order' => (int) $sortOrder];\n            }\n        }\n\n        if ($upsert) {\n            foreach ($upsert as $update) {\n                $this->newQuery()\n                    ->where($keyName, $update['id'])\n                    ->update([$this->getSortOrderColumn() => $update['sort_order']]);\n            }\n        }\n\n        $this->fireEvent('model.setSortableOrder');\n    }\n\n    /**\n     * processSortableOrdersInternal\n     */\n    protected function processSortableOrdersInternal($itemIds, $referencePool = null): array\n    {\n        // Build incrementing reference pool\n        if ($referencePool === true) {\n            $referencePool = range(1, count($itemIds));\n        }\n        else {\n            // Extract a reference pool from the database\n            if (!$referencePool) {\n                $referencePool = $this->newQuery()\n                    ->whereIn($this->getKeyName(), $itemIds)\n                    ->pluck($this->getSortOrderColumn())\n                    ->all();\n            }\n\n            // Check for corrupt values, if found, reset with a unique pool\n            $referencePool = array_unique(array_filter($referencePool, 'strlen'));\n            if (count($referencePool) !== count($itemIds)) {\n                $referencePool = $itemIds;\n            }\n\n            // Sort pool to apply against the sorted items\n            sort($referencePool);\n        }\n\n        // Process the item orders to a sort key map\n        $result = [];\n        foreach ($itemIds as $index => $id) {\n            $result[$id] = $referencePool[$index];\n        }\n\n        return $result;\n    }\n\n    /**\n     * resetSortableOrdering can be used to repair corrupt or missing sortable definitions.\n     */\n    public function resetSortableOrdering()\n    {\n        $ids = $this->newQuery()->pluck($this->getKeyName());\n\n        foreach ($ids as $id) {\n            $this->newQuery()->where($this->getKeyName(), $id)->update([$this->getSortOrderColumn() => $id]);\n        }\n    }\n\n    /**\n     * getSortOrderColumn name of the \"sort order\" column.\n     * @return string\n     */\n    public function getSortOrderColumn()\n    {\n        return defined('static::SORT_ORDER') ? static::SORT_ORDER : 'sort_order';\n    }\n\n    /**\n     * getQualifiedSortOrderColumn gets the fully qualified \"sort order\" column.\n     * @return string\n     */\n    public function getQualifiedSortOrderColumn()\n    {\n        return $this->qualifyColumn($this->getSortOrderColumn());\n    }\n}\n"
  },
  {
    "path": "src/Database/Traits/SortableRelation.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\nuse Db;\nuse Exception;\n\n/**\n * SortableRelation adds sorting support to pivot relationships\n *\n * Usage:\n *\n * In the model class definition add:\n *\n *   use \\October\\Rain\\Database\\Traits\\SortableRelation;\n *\n *   public $belongsToMany = [..., 'pivotSortable' => 'sort_order_column'];\n *\n * To set orders:\n *\n *   $model->setSortableRelationOrder($relationName, $recordIds, $recordOrders);\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait SortableRelation\n{\n    /**\n     * @var array sortableRelationDefinitions\n     */\n    protected $sortableRelationDefinitions;\n\n    /**\n     * initializeSortableRelation trait for the model.\n     */\n    public function initializeSortableRelation()\n    {\n        $this->bindEvent('model.afterInit', function() {\n            $this->defineSortableRelations();\n        });\n\n        $this->bindEvent('model.relation.attach', function ($relationName, $attached, $data) {\n            if (!array_key_exists($relationName, $this->getSortableRelations())) {\n                return;\n            }\n\n            // Order already set in pivot data (assuming singular)\n            $column = $this->getRelationSortOrderColumn($relationName);\n            if (is_array($data) && array_key_exists($column, $data)) {\n                return;\n            }\n\n            // Calculate a new order\n            $relation = $this->$relationName();\n            $order = $relation->max($relation->qualifyPivotColumn($column));\n            foreach ((array) $attached as $id) {\n                $relation->updateExistingPivot($id, [$column => ++$order]);\n            }\n        });\n    }\n\n    /**\n     * defineSortableRelations will spin over every relation and check for pivotSortable mode\n     */\n    protected function defineSortableRelations()\n    {\n        $interactsWithPivot = ['belongsToMany', 'morphToMany'];\n        $sortableRelations = [];\n\n        foreach ($interactsWithPivot as $type) {\n            foreach ($this->$type as $name => $definition) {\n                if (!isset($definition['pivotSortable'])) {\n                    continue;\n                }\n\n                $sortableRelations[$name] = $attrName = $definition['pivotSortable'];\n\n                // Ensure attribute is included in pivot definition\n                if (!isset($definition['pivot']) || !in_array($attrName, $definition['pivot'])) {\n                    $this->$type[$name]['pivot'][] = $attrName;\n                }\n\n                // Apply sort by the pivot table column name\n                if (!isset($definition['order'])) {\n                    $tableName = $definition['table'] ?? $this->$name()->getTable();\n                    $this->$type[$name]['order'][] = $tableName.'.'.$attrName;\n                }\n            }\n        }\n\n        $this->sortableRelationDefinitions = $sortableRelations;\n    }\n\n    /**\n     * setSortableRelationOrder sets the sort order of records to the specified orders. If the orders is\n     * undefined, the record identifier is used. If reference pool is true, then an incrementing\n     * pool is used.\n     * @param string $relationName\n     * @param mixed $itemIds\n     * @param  array|null|bool $referencePool\n     */\n    public function setSortableRelationOrder($relationName, $itemIds, $referencePool = null)\n    {\n        if (!$this->isSortableRelation($relationName)) {\n            throw new Exception(\"Invalid setSortableRelationOrder call - the relation '{$relationName}' is not sortable\");\n        }\n\n        if (!is_array($itemIds)) {\n            return;\n        }\n\n        $sortKeyMap = $this->processSortableRelationOrdersInternal($relationName, $itemIds, $referencePool);\n        if (count($itemIds) !== count($sortKeyMap)) {\n            throw new Exception('Invalid setSortableRelationOrder call - count of itemIds do not match count of itemOrders');\n        }\n\n        $upsert = [];\n        foreach ($itemIds as $id) {\n            $sortOrder = $sortKeyMap[$id] ?? null;\n            if ($sortOrder !== null) {\n                $upsert[] = ['id' => $id, 'sort_order' => (int) $sortOrder];\n            }\n        }\n\n        if ($upsert) {\n            foreach ($upsert as $update) {\n                $result = $this->exists ? $this->$relationName()->updateExistingPivot($update['id'], [\n                    $this->getRelationSortOrderColumn($relationName) => $update['sort_order']\n                ]) : 0;\n\n                if (!$result && $this->sessionKey) {\n                    Db::table('deferred_bindings')\n                        ->where('master_field', $relationName)\n                        ->where('master_type', get_class($this))\n                        ->where('session_key', $this->sessionKey)\n                        ->where('slave_id', $update['id'])\n                        ->limit(1)\n                        ->update(['sort_order' => $update['sort_order']]);\n                }\n            }\n        }\n    }\n\n    /**\n     * processSortableRelationOrdersInternal\n     */\n    protected function processSortableRelationOrdersInternal($relationName, $itemIds, $referencePool = null): array\n    {\n        // Build incrementing reference pool\n        if ($referencePool === true) {\n            $referencePool = range(1, count($itemIds));\n        }\n        else {\n            // Extract a reference pool from the database\n            if (!$referencePool) {\n                $referencePool = $this->$relationName()\n                    ->whereIn($this->getKeyName(), $itemIds)\n                    ->pluck($this->getRelationSortOrderColumn($relationName))\n                    ->all();\n            }\n\n            // Check for corrupt values, if found, reset with a unique pool\n            $referencePool = array_unique(array_filter($referencePool, 'strlen'));\n            if (count($referencePool) !== count($itemIds)) {\n                $referencePool = $itemIds;\n            }\n\n            // Sort pool to apply against the sorted items\n            sort($referencePool);\n        }\n\n        // Process the item orders to a sort key map\n        $result = [];\n        foreach ($itemIds as $index => $id) {\n            $result[$id] = $referencePool[$index];\n        }\n\n        return $result;\n    }\n\n    /**\n     * isSortableRelation returns true if the supplied relation is sortable.\n     */\n    public function isSortableRelation($relationName)\n    {\n        return isset($this->sortableRelationDefinitions[$relationName]);\n    }\n\n    /**\n     * getRelationSortOrderColumn gets the name of the \"sort_order\" column.\n     */\n    public function getRelationSortOrderColumn(string $relation): string\n    {\n        return $this->sortableRelationDefinitions[$relation] ?? 'sort_order';\n    }\n\n    /**\n     * getSortableRelations returns all configured sortable relations.\n     */\n    protected function getSortableRelations(): array\n    {\n        return $this->sortableRelationDefinitions;\n    }\n}\n"
  },
  {
    "path": "src/Database/Traits/Translatable.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\nuse App;\nuse Db;\nuse Site;\nuse Exception;\n\n/**\n * Translatable trait provides per-row model translation using a single\n * translation table. Locale is resolved via Site/SiteManager.\n *\n * Usage:\n *\n *     use \\October\\Rain\\Database\\Traits\\Translatable;\n *\n *     public $translatable = ['name', 'description'];\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait Translatable\n{\n    /**\n     * @var string translatableContext is the active locale override\n     */\n    protected $translatableContext;\n\n    /**\n     * @var string translatableDefault is the default locale cache\n     */\n    protected $translatableDefault;\n\n    /**\n     * @var array translatableAttributes stores loaded translation data keyed by locale\n     */\n    protected $translatableAttributes = [];\n\n    /**\n     * @var array translatableOriginals stores original translation data for dirty checking\n     */\n    protected $translatableOriginals = [];\n\n    /**\n     * @var array translatableBaseValues stashes default-locale values when a non-default\n     * locale has been promoted into $attributes\n     */\n    protected $translatableBaseValues = [];\n\n    /**\n     * initializeTranslatable trait for a model\n     */\n    public function initializeTranslatable()\n    {\n        if (!is_array($this->translatable)) {\n            throw new Exception(sprintf(\n                'The $translatable property in %s must be an array to use the Translatable trait.',\n                static::class\n            ));\n        }\n\n        $this->morphMany['translations'] = [\n            $this->getTranslateAttributeModelClass(),\n            'name' => 'model',\n            'delete' => true\n        ];\n\n        // Promote translated values into $attributes after fetch\n        $this->bindEvent('model.afterFetch', function() {\n            $this->promoteTranslatableValues();\n        });\n\n        // Demote + persist translations before save\n        $this->bindEvent('model.saveInternal', function() {\n            $this->syncTranslatableAttributes();\n        });\n    }\n\n    //\n    // Locale resolution\n    //\n\n    /**\n     * getTranslatableContext returns the active locale, resolved lazily from Site.\n     * Lazy resolution avoids issues during migrations/seeds when Site isn't booted.\n     */\n    public function getTranslatableContext()\n    {\n        if ($this->translatableContext === null) {\n            $this->translatableContext = $this->resolveTranslatableLocale();\n        }\n\n        return $this->translatableContext;\n    }\n\n    /**\n     * getTranslatableDefault returns the default locale, resolved lazily from Site.\n     */\n    public function getTranslatableDefault()\n    {\n        if ($this->translatableDefault === null) {\n            $this->translatableDefault = $this->resolveTranslatableDefaultLocale();\n        }\n\n        return $this->translatableDefault;\n    }\n\n    /**\n     * resolveTranslatableLocale reads the current locale from the Site facade.\n     * Override this to change how the active locale is determined.\n     */\n    protected function resolveTranslatableLocale()\n    {\n        $site = Site::getSiteFromContext();\n\n        return $site ? $site->hard_locale : $this->getTranslatableDefault();\n    }\n\n    /**\n     * resolveTranslatableDefaultLocale reads the default locale from the Site facade.\n     * Override this to change how the default locale is determined.\n     */\n    protected function resolveTranslatableDefaultLocale()\n    {\n        $site = Site::getPrimarySite();\n\n        return $site ? $site->hard_locale : 'en';\n    }\n\n    //\n    // Activation & bypass\n    //\n\n    /**\n     * isTranslatableEnabled returns true to indicate the trait is active\n     */\n    public function isTranslatableEnabled()\n    {\n        return true;\n    }\n\n    /**\n     * shouldTranslate returns true when the active locale differs from the default.\n     * Returns false for single-locale installs so the trait is invisible.\n     */\n    public function shouldTranslate()\n    {\n        if (!$this->isTranslatableEnabled()) {\n            return false;\n        }\n\n        return $this->getTranslatableContext() !== $this->getTranslatableDefault();\n    }\n\n    /**\n     * isTranslatableAttribute checks if a specific attribute should be translated right now.\n     * Returns false when: default locale active, or attribute not in $translatable.\n     */\n    public function isTranslatableAttribute($key)\n    {\n        if ($key === 'translatable' || !$this->shouldTranslate()) {\n            return false;\n        }\n\n        return in_array($key, $this->getTranslatableAttributes());\n    }\n\n    /**\n     * getTranslatableAttributes returns the translatable attribute names\n     */\n    public function getTranslatableAttributes()\n    {\n        return $this->translatable;\n    }\n\n    //\n    // Promote & demote\n    //\n\n    /**\n     * promoteTranslatableValues swaps translated values into $attributes\n     * and stashes the base (default-locale) values for later restoration.\n     * Called after fetch and when setLocale() changes context.\n     */\n    protected function promoteTranslatableValues()\n    {\n        if (!$this->shouldTranslate()) {\n            // Default locale active: restore base values if previously promoted\n            if (!empty($this->translatableBaseValues)) {\n                $this->restoreTranslatableBaseValues();\n            }\n            return;\n        }\n\n        $locale = $this->getTranslatableContext();\n        $translatable = $this->getTranslatableAttributes();\n\n        // Stash current base values\n        foreach ($translatable as $key) {\n            if (array_key_exists($key, $this->attributes)) {\n                $this->translatableBaseValues[$key] = $this->attributes[$key];\n            }\n        }\n\n        // Load translations for this locale if not already loaded\n        if (!array_key_exists($locale, $this->translatableAttributes)) {\n            $this->loadTranslatableData($locale);\n        }\n\n        // Promote: overwrite $attributes with translated values\n        $translated = $this->translatableAttributes[$locale] ?? [];\n        foreach ($translatable as $key) {\n            if (array_key_exists($key, $translated)) {\n                $this->attributes[$key] = $translated[$key];\n            }\n        }\n    }\n\n    /**\n     * demoteTranslatableValues reads current $attributes back into the\n     * translation cache and restores base (default-locale) values for the DB write\n     */\n    protected function demoteTranslatableValues()\n    {\n        if (!$this->shouldTranslate() || empty($this->translatableBaseValues)) {\n            return;\n        }\n\n        $locale = $this->getTranslatableContext();\n        $translatable = $this->getTranslatableAttributes();\n\n        // Read current (possibly modified) translated values back from $attributes\n        foreach ($translatable as $key) {\n            if (array_key_exists($key, $this->attributes)) {\n                $this->translatableAttributes[$locale][$key] = $this->attributes[$key];\n            }\n        }\n\n        // Restore base values to $attributes for the DB write\n        $this->restoreTranslatableBaseValues();\n    }\n\n    /**\n     * restoreTranslatableBaseValues restores the stashed default-locale values\n     * back into $attributes\n     */\n    protected function restoreTranslatableBaseValues()\n    {\n        foreach ($this->translatableBaseValues as $key => $value) {\n            $this->attributes[$key] = $value;\n        }\n\n        $this->translatableBaseValues = [];\n    }\n\n    //\n    // Base value access\n    //\n\n    /**\n     * getTranslatableBaseValue returns the default-locale value for a translatable\n     * attribute. When a non-default locale is promoted, reads from the stash.\n     * When the default locale is active, reads from $attributes directly.\n     */\n    public function getTranslatableBaseValue(string $key)\n    {\n        if (!empty($this->translatableBaseValues) && array_key_exists($key, $this->translatableBaseValues)) {\n            return $this->translatableBaseValues[$key];\n        }\n\n        return $this->attributes[$key] ?? null;\n    }\n\n    //\n    // Reading translations\n    //\n\n    /**\n     * getTranslation returns the translated value for an attribute and locale\n     */\n    public function getTranslation($key, $locale, $useFallback = true)\n    {\n        // Active promoted locale: read from $attributes\n        if ($locale === $this->getTranslatableContext() && $this->shouldTranslate()) {\n            $result = $this->attributes[$key] ?? null;\n        }\n        // Default locale: read from base values\n        elseif ($locale === $this->getTranslatableDefault()) {\n            $result = $this->getTranslatableBaseValue($key);\n        }\n        // Other locale: read from sidecar cache\n        else {\n            if (!array_key_exists($locale, $this->translatableAttributes)) {\n                $this->loadTranslatableData($locale);\n            }\n\n            if ($this->hasTranslation($key, $locale)) {\n                $result = $this->translatableAttributes[$locale][$key] ?? null;\n            }\n            elseif ($useFallback) {\n                $result = $this->getTranslatableBaseValue($key);\n            }\n            else {\n                $result = null;\n            }\n        }\n\n        // Handle jsonable attributes\n        if (\n            is_string($result) &&\n            method_exists($this, 'isJsonable') &&\n            $this->isJsonable($key)\n        ) {\n            $result = json_decode($result, true);\n        }\n\n        return $result;\n    }\n\n    /**\n     * getTranslations returns all locale values for a single attribute\n     */\n    public function getTranslations($key)\n    {\n        $translations = [];\n\n        // Default locale from base values\n        $defaultLocale = $this->getTranslatableDefault();\n        $defaultValue = $this->getTranslatableBaseValue($key);\n        if ($defaultValue !== null) {\n            $translations[$defaultLocale] = $defaultValue;\n        }\n\n        // Other locales from translation table\n        $rows = $this->translations->where('attribute', $key);\n        foreach ($rows as $row) {\n            $translations[$row->locale] = $row->value;\n        }\n\n        // Handle jsonable attributes\n        if (method_exists($this, 'isJsonable') && $this->isJsonable($key)) {\n            foreach ($translations as $locale => $value) {\n                if (is_string($value)) {\n                    $translations[$locale] = json_decode($value, true);\n                }\n            }\n        }\n\n        return $translations;\n    }\n\n    /**\n     * hasTranslation checks if a translation row exists for one attribute\n     */\n    public function hasTranslation($key, $locale = null)\n    {\n        if ($locale === null) {\n            $locale = $this->getTranslatableContext();\n        }\n\n        // Active promoted locale: check $attributes\n        if ($locale === $this->getTranslatableContext() && $this->shouldTranslate()) {\n            $value = $this->attributes[$key] ?? null;\n            return $value !== null && $value !== '';\n        }\n\n        // Default locale: check base values\n        if ($locale === $this->getTranslatableDefault()) {\n            $value = $this->getTranslatableBaseValue($key);\n            return $value !== null && $value !== '';\n        }\n\n        // Other locale: check sidecar\n        if (!array_key_exists($locale, $this->translatableAttributes)) {\n            $this->loadTranslatableData($locale);\n        }\n\n        $value = $this->translatableAttributes[$locale][$key] ?? null;\n\n        return $value !== null && $value !== '';\n    }\n\n    /**\n     * hasTranslations checks if any translation rows exist for the locale (record-level)\n     */\n    public function hasTranslations($locale = null)\n    {\n        if ($locale === null) {\n            $locale = $this->getTranslatableContext();\n        }\n\n        if ($this->relationLoaded('translations')) {\n            return $this->translations->where('locale', $locale)->isNotEmpty();\n        }\n\n        return Db::table($this->getTranslateAttributeTable())\n            ->where('model_type', $this->getMorphClass())\n            ->where('model_id', $this->getKey())\n            ->where('locale', $locale)\n            ->exists();\n    }\n\n    /**\n     * getTranslatedLocales returns locales with translations. When a key is provided,\n     * returns locales for that specific attribute. Without a key, returns all locales\n     * that have any translation rows (record-level).\n     */\n    public function getTranslatedLocales($key = null)\n    {\n        if ($this->relationLoaded('translations')) {\n            $query = $this->translations;\n\n            if ($key !== null) {\n                $query = $query->where('attribute', $key);\n            }\n\n            return $query->pluck('locale')->unique()->values()->toArray();\n        }\n\n        $query = Db::table($this->getTranslateAttributeTable())\n            ->where('model_type', $this->getMorphClass())\n            ->where('model_id', $this->getKey());\n\n        if ($key !== null) {\n            $query->where('attribute', $key);\n        }\n\n        return $query->pluck('locale')->unique()->toArray();\n    }\n\n    //\n    // Writing translations\n    //\n\n    /**\n     * setTranslation sets a translated value for an attribute and locale\n     */\n    public function setTranslation($key, $locale, $value)\n    {\n        // Writing to the active promoted locale: write to $attributes directly\n        if ($locale === $this->getTranslatableContext() && $this->shouldTranslate()) {\n            $this->attributes[$key] = $value;\n            return $value;\n        }\n\n        // Writing to the default locale: write to base values\n        if ($locale === $this->getTranslatableDefault()) {\n            if (!empty($this->translatableBaseValues)) {\n                $this->translatableBaseValues[$key] = $value;\n            }\n            else {\n                $this->attributes[$key] = $value;\n            }\n            return $value;\n        }\n\n        // Writing to a different non-active locale: write to sidecar cache\n        if (!array_key_exists($locale, $this->translatableAttributes)) {\n            $this->loadTranslatableData($locale);\n        }\n\n        $this->translatableAttributes[$locale][$key] = $value;\n\n        return $value;\n    }\n\n    /**\n     * setTranslations sets multiple locale values at once for a single attribute\n     */\n    public function setTranslations($key, array $translations)\n    {\n        foreach ($translations as $locale => $value) {\n            $this->setTranslation($key, $locale, $value);\n        }\n    }\n\n    //\n    // Deleting translations\n    //\n\n    /**\n     * forgetTranslation deletes a single translation row\n     */\n    public function forgetTranslation($key, $locale)\n    {\n        Db::table($this->getTranslateAttributeTable())\n            ->where('model_type', $this->getMorphClass())\n            ->where('model_id', $this->getKey())\n            ->where('locale', $locale)\n            ->where('attribute', $key)\n            ->delete();\n\n        unset($this->translatableAttributes[$locale][$key]);\n        unset($this->translatableOriginals[$locale][$key]);\n    }\n\n    /**\n     * forgetTranslations deletes all translation rows for an attribute (all locales)\n     */\n    public function forgetTranslations($key)\n    {\n        Db::table($this->getTranslateAttributeTable())\n            ->where('model_type', $this->getMorphClass())\n            ->where('model_id', $this->getKey())\n            ->where('attribute', $key)\n            ->delete();\n\n        foreach ($this->translatableAttributes as $locale => &$data) {\n            unset($data[$key]);\n        }\n\n        foreach ($this->translatableOriginals as $locale => &$data) {\n            unset($data[$key]);\n        }\n    }\n\n    /**\n     * forgetAllTranslations deletes all translation rows for a locale (\"unpublish French\")\n     */\n    public function forgetAllTranslations($locale)\n    {\n        Db::table($this->getTranslateAttributeTable())\n            ->where('model_type', $this->getMorphClass())\n            ->where('model_id', $this->getKey())\n            ->where('locale', $locale)\n            ->delete();\n\n        unset($this->translatableAttributes[$locale]);\n        unset($this->translatableOriginals[$locale]);\n    }\n\n    //\n    // Locale context\n    //\n\n    /**\n     * setLocale overrides the locale context for this model instance\n     */\n    public function setLocale($locale)\n    {\n        // Demote current promoted values back to sidecar\n        if ($this->shouldTranslate() && !empty($this->translatableBaseValues)) {\n            $this->demoteTranslatableValues();\n        }\n\n        $this->translatableContext = $locale;\n\n        // Re-promote with new locale\n        if ($this->exists) {\n            $this->promoteTranslatableValues();\n        }\n\n        $this->fireEvent('model.translate.contextChange', [$locale]);\n\n        return $this;\n    }\n\n    /**\n     * getLocale returns the active locale (context override or site locale)\n     */\n    public function getLocale()\n    {\n        return $this->getTranslatableContext();\n    }\n\n    //\n    // Dirty checking\n    //\n\n    /**\n     * isTranslateDirty determines if the model or a given translated attribute\n     * has been modified for a locale\n     */\n    public function isTranslateDirty($attribute = null, $locale = null)\n    {\n        $dirty = $this->getTranslateDirty($locale);\n\n        if (is_null($attribute)) {\n            return count($dirty) > 0;\n        }\n\n        return array_key_exists($attribute, $dirty);\n    }\n\n    /**\n     * getTranslateDirty returns the translated attributes that have been changed\n     * since last sync\n     */\n    public function getTranslateDirty($locale = null)\n    {\n        if (!$locale) {\n            $locale = $this->getTranslatableContext();\n        }\n\n        if (!array_key_exists($locale, $this->translatableAttributes)) {\n            return [];\n        }\n\n        // All dirty when no originals recorded\n        if (!array_key_exists($locale, $this->translatableOriginals)) {\n            return $this->translatableAttributes[$locale];\n        }\n\n        $dirty = [];\n        foreach ($this->translatableAttributes[$locale] as $key => $value) {\n            if (!array_key_exists($key, $this->translatableOriginals[$locale])) {\n                $dirty[$key] = $value;\n            }\n            elseif ($value != $this->translatableOriginals[$locale][$key]) {\n                $dirty[$key] = $value;\n            }\n        }\n\n        return $dirty;\n    }\n\n    /**\n     * getTranslatableOriginals gets the original values of the translated attributes\n     */\n    public function getTranslatableOriginals($locale = null)\n    {\n        if (!$locale) {\n            return $this->translatableOriginals;\n        }\n\n        return $this->translatableOriginals[$locale] ?? null;\n    }\n\n    //\n    // Data storage\n    //\n\n    /**\n     * syncTranslatableAttributes demotes translated values and stores them\n     * in the translation table before the base model save\n     */\n    protected function syncTranslatableAttributes()\n    {\n        // Demote: restore base values before save\n        $this->demoteTranslatableValues();\n\n        // Store translations for each known locale. When the model has no key\n        // yet (new record), defer until after insert assigns the primary key\n        if ($this->getKey()) {\n            $this->storeTranslatableBasicData();\n        }\n        else {\n            $this->bindEventOnce('model.saveComplete', function() {\n                $this->storeTranslatableBasicData();\n            });\n        }\n    }\n\n    /**\n     * storeTranslatableBasicData stores translations for each known dirty locale\n     */\n    protected function storeTranslatableBasicData()\n    {\n        $knownLocales = array_keys($this->translatableAttributes);\n        foreach ($knownLocales as $locale) {\n            if (!$this->isTranslateDirty(null, $locale)) {\n                continue;\n            }\n\n            $this->fireEvent('model.translate.beforeSave', [$locale]);\n            $this->storeTranslatableData($locale);\n            $this->fireEvent('model.translate.afterSave', [$locale]);\n        }\n    }\n\n\n    /**\n     * storeTranslatableData saves translation data for a single locale using upsert\n     */\n    protected function storeTranslatableData($locale)\n    {\n        $dirty = $this->getTranslateDirty($locale);\n\n        if (empty($dirty)) {\n            return;\n        }\n\n        $isDefaultLocale = ($locale === $this->getTranslatableDefault());\n\n        $rows = [];\n        foreach ($dirty as $key => $value) {\n            // For non-default locales, skip attributes whose value matches the\n            // model's local attribute (the default locale value). No row = inherits\n            // from default, so changes to the default automatically propagate.\n            if (!$isDefaultLocale) {\n                $defaultValue = $this->getTranslatableBaseValue($key);\n                if ($value === $defaultValue) {\n                    continue;\n                }\n            }\n\n            // Serialize array values for storage\n            $storeValue = is_array($value) ? json_encode($value) : $value;\n\n            $rows[] = [\n                'model_type' => $this->getMorphClass(),\n                'model_id' => $this->getKey(),\n                'locale' => $locale,\n                'attribute' => $key,\n                'value' => $storeValue,\n            ];\n        }\n\n        if (empty($rows)) {\n            return;\n        }\n\n        Db::table($this->getTranslateAttributeTable())->upsert(\n            $rows,\n            ['model_type', 'model_id', 'locale', 'attribute'],\n            ['value']\n        );\n    }\n\n    /**\n     * loadTranslatableData loads translation data for a locale, using the eager-loaded\n     * relationship when available, falling back to a direct query\n     */\n    protected function loadTranslatableData($locale)\n    {\n        if ($this->relationLoaded('translations')) {\n            $rows = $this->translations\n                ->where('locale', $locale)\n                ->pluck('value', 'attribute')\n                ->toArray();\n        }\n        else {\n            $rows = Db::table($this->getTranslateAttributeTable())\n                ->where('model_type', $this->getMorphClass())\n                ->where('model_id', $this->getKey())\n                ->where('locale', $locale)\n                ->pluck('value', 'attribute')\n                ->toArray();\n        }\n\n        $this->translatableAttributes[$locale] = $rows;\n        $this->translatableOriginals[$locale] = $rows;\n    }\n\n    //\n    // Query scopes\n    //\n\n    /**\n     * scopeWhereTranslation adds a where clause for a translated attribute\n     */\n    public function scopeWhereTranslation($query, $key, $locale, $value, $operator = '=')\n    {\n        return $query->whereExists(function ($q) use ($key, $locale, $value, $operator) {\n            $table = $this->getTranslateAttributeTable();\n\n            $q->select(Db::raw(1))\n                ->from($table)\n                ->whereColumn($table . '.model_id', $this->getQualifiedKeyName())\n                ->where($table . '.model_type', $this->getMorphClass())\n                ->where($table . '.locale', $locale)\n                ->where($table . '.attribute', $key)\n                ->where($table . '.value', $operator, $value);\n        });\n    }\n\n    /**\n     * scopeOrderByTranslation adds an order by clause for a translated attribute\n     */\n    public function scopeOrderByTranslation($query, $key, $locale, $direction = 'asc')\n    {\n        $table = $this->getTranslateAttributeTable();\n        $alias = 'translate_order_' . $key;\n\n        return $query\n            ->leftJoin($table . ' as ' . $alias, function ($join) use ($alias, $key, $locale) {\n                $join->on($alias . '.model_id', '=', $this->getQualifiedKeyName())\n                    ->where($alias . '.model_type', '=', $this->getMorphClass())\n                    ->where($alias . '.locale', '=', $locale)\n                    ->where($alias . '.attribute', '=', $key);\n            })\n            ->orderBy($alias . '.value', $direction);\n    }\n\n    /**\n     * scopeWithTranslation eager loads translations for a single locale\n     */\n    public function scopeWithTranslation($query, $locale = null)\n    {\n        if ($locale === null) {\n            $locale = $this->getTranslatableContext();\n        }\n\n        return $query->with(['translations' => function ($q) use ($locale) {\n            $q->where('locale', $locale);\n        }]);\n    }\n\n    /**\n     * scopeWithTranslations eager loads all translations (all locales)\n     */\n    public function scopeWithTranslations($query)\n    {\n        return $query->with('translations');\n    }\n\n    //\n    // Helpers\n    //\n\n    /**\n     * getTranslateAttributeModelClass returns the model class used for translations.\n     * Resolved via the 'translate.attribute' container binding when available,\n     * falling back to the base TranslateAttribute model. Override per-model\n     * to use a custom translation table.\n     */\n    public function getTranslateAttributeModelClass()\n    {\n        if (App::bound('core.translate.attribute')) {\n            return App::make('core.translate.attribute');\n        }\n\n        return \\October\\Rain\\Database\\Models\\TranslateAttribute::class;\n    }\n\n    /**\n     * getTranslateAttributeTable returns the table name for translation storage\n     */\n    public function getTranslateAttributeTable()\n    {\n        $modelClass = $this->getTranslateAttributeModelClass();\n\n        return (new $modelClass)->getTable();\n    }\n}\n"
  },
  {
    "path": "src/Database/Traits/UserFootprints.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\nuse App;\n\n/**\n * UserFootprints adds created_user_id and updated_user_id fields to a model and populates\n * them using the logged in user via the backend.auth provider.\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait UserFootprints\n{\n    /**\n     * initializeUserFootprints trait for a model.\n     */\n    public function initializeUserFootprints()\n    {\n        $this->bindEvent('model.saveInternal', function () {\n            $this->updateUserFootprints();\n        });\n\n        $userModel = $this->getUserFootprintAuth()->getProvider()->getModel();\n\n        $this->belongsTo['updated_user'] = [\n            $userModel,\n            'replicate' => false\n        ];\n\n        $this->belongsTo['created_user'] = [\n            $userModel,\n            'replicate' => false\n        ];\n    }\n\n    /**\n     * updateUserFootprints\n     */\n    public function updateUserFootprints()\n    {\n        $userId = $this->getUserFootprintAuth()->id();\n        if (!$userId) {\n            return;\n        }\n\n        $updatedColumn = $this->getUpdatedUserIdColumn();\n        if ($updatedColumn !== null && !$this->isDirty($updatedColumn)) {\n            $this->{$updatedColumn} = $userId;\n        }\n\n        $createdColumn = $this->getCreatedUserIdColumn();\n        if (!$this->exists && $createdColumn !== null && !$this->isDirty($createdColumn)) {\n            $this->{$createdColumn} = $userId;\n        }\n    }\n\n    /**\n     * getCreatedUserIdColumn gets the name of the \"created user id\" column.\n     * @return string\n     */\n    public function getCreatedUserIdColumn()\n    {\n        return defined('static::CREATED_USER_ID') ? static::CREATED_USER_ID : 'created_user_id';\n    }\n\n    /**\n     * getCreatedUserIdColumn gets the name of the \"updated user id\" column.\n     * @return string\n     */\n    public function getUpdatedUserIdColumn()\n    {\n        return defined('static::UPDATED_USER_ID') ? static::UPDATED_USER_ID : 'updated_user_id';\n    }\n\n    /**\n     * getUserFootprintAuth\n     */\n    protected function getUserFootprintAuth()\n    {\n        return App::make('backend.auth');\n    }\n}\n"
  },
  {
    "path": "src/Database/Traits/Validation.php",
    "content": "<?php namespace October\\Rain\\Database\\Traits;\n\nuse App;\nuse Lang;\nuse Input;\nuse Validator;\nuse October\\Rain\\Database\\ModelException;\nuse Illuminate\\Support\\MessageBag;\nuse Exception;\n\n/**\n * Validation trait for models\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait Validation\n{\n    /**\n     * @var array rules for validation\n     *\n     * public $rules = [];\n     */\n\n    /**\n     * @var array attributeNames of custom attributes\n     *\n     * public $attributeNames = [];\n     */\n\n    /**\n     * @var array customMessages of custom error messages\n     *\n     * public $customMessages = [];\n     */\n\n    /**\n     * @var bool throwOnValidation makes the validation procedure throw an {@link October\\Rain\\Database\\ModelException}\n     * instead of returning false when validation fails\n     *\n     * public $throwOnValidation = true;\n     */\n\n    /**\n     * @var bool validationForced is an internal marker to indicate if force option was used.\n     */\n    public $validationForced = false;\n\n    /**\n     * @var \\Illuminate\\Support\\MessageBag validationErrors message bag instance containing\n     * validation error messages\n     */\n    protected $validationErrors;\n\n    /**\n     * @var array validationDefaultAttrNames default custom attribute names\n     */\n    protected $validationDefaultAttrNames = [];\n\n    /**\n     * initializeValidation for this model\n     */\n    public function initializeValidation()\n    {\n        if (!is_array($this->rules)) {\n            throw new Exception(sprintf(\n                'The $rules property in %s must be an array to use the Validation trait.',\n                static::class\n            ));\n        }\n\n        $this->bindEvent('model.saveInternal', function() {\n            $validationForced = $this->validationForced;\n\n            if (($forceOption = $this->getSaveOption('force')) !== null) {\n                $this->validationForced = $forceOption;\n            }\n\n            // If forcing the save event, the beforeValidate/afterValidate\n            // events should still fire for consistency. So validate an\n            // empty set of rules and messages.\n            if ($this->validationForced) {\n                $valid = $this->validate([], []);\n            }\n            else {\n                $valid = $this->validate();\n            }\n\n            $this->validationForced = $validationForced;\n\n            if (!$valid) {\n                return false;\n            }\n        }, 500);\n    }\n\n    /**\n     * setValidationAttributeNames programmatically sets multiple validation attribute names\n     * @param array $attributeNames\n     */\n    public function setValidationAttributeNames($attributeNames)\n    {\n        $this->validationDefaultAttrNames = $attributeNames;\n    }\n\n    /**\n     * setValidationAttributeName programmatically sets the validation attribute names, will take\n     * lower priority to model defined attribute names found in `$attributeNames`\n     * @param string $attr\n     * @param string $name\n     * @return void\n     */\n    public function setValidationAttributeName($attr, $name)\n    {\n        $this->validationDefaultAttrNames[$attr] = $name;\n    }\n\n    /**\n     * getValidationAttributes returns the model data used for validation\n     * @return array\n     */\n    protected function getValidationAttributes()\n    {\n        return $this->getAttributes();\n    }\n\n    /**\n     * addValidationRule will append a rule to the stack and reset the value as a processed array\n     */\n    public function addValidationRule(string $name, $definition)\n    {\n        $rules = $this->rules[$name] ?? [];\n        if (!is_array($rules)) {\n            $rules = explode('|', $rules);\n        }\n\n        if (is_array($definition)) {\n            $rules = array_merge($rules, $definition);\n        }\n        else {\n            $rules[] = $definition;\n        }\n\n        $this->rules[$name] = $rules;\n    }\n\n    /**\n     * removeValidationRule removes a validation rule from the stack and resets the value as a processed array\n     */\n    public function removeValidationRule(string $name, $definition = '*')\n    {\n        if ($definition === '*') {\n            unset($this->rules[$name]);\n            return;\n        }\n\n        $rules = $this->rules[$name] ?? [];\n        if (!is_array($rules)) {\n            $rules = explode('|', $rules);\n        }\n\n        foreach ($rules as $key => $rule) {\n            if ($rule === $definition) {\n                unset($rules[$key]);\n            }\n            elseif (\n                is_string($definition) &&\n                is_string($rule) &&\n                str_starts_with($rule, \"{$definition}:\")\n            ) {\n                unset($rules[$key]);\n            }\n        }\n\n        $this->rules[$name] = $rules;\n    }\n\n    /**\n     * getRelationValidationValue handles attachments that validate differently to their simple values\n     */\n    protected function getRelationValidationValue($relationName)\n    {\n        // Locate records, with deferred logic\n        if (\n            $this->sessionKey &&\n            !$this->relationLoaded($relationName) &&\n            $this->hasDeferred($this->sessionKey, $relationName)\n        ) {\n            $data = $this->$relationName()->withDeferred($this->sessionKey)->get();\n        }\n        else {\n            $data = $this->$relationName;\n        }\n\n        // DRY logic to post-process validation data\n        $processValidationValue = function($value) {\n            // Attachments\n            if ($value instanceof \\October\\Rain\\Database\\Attach\\File) {\n                $localPath = $value->getLocalPath();\n\n                // Exception handling for UploadedFile\n                if (file_exists($localPath)) {\n                    return new \\Symfony\\Component\\HttpFoundation\\File\\UploadedFile(\n                        $localPath,\n                        $value->file_name,\n                        $value->content_type,\n                        null,\n                        true\n                    );\n                }\n\n                // Fallback to string\n                $value = $localPath;\n            }\n\n            return $value;\n        };\n\n        // Process singular\n        if ($this->isRelationTypeSingular($relationName)) {\n            if ($data instanceof \\Illuminate\\Support\\Collection) {\n                $data = $data->last();\n            }\n\n            return $processValidationValue($data);\n        }\n\n        // Cast to primitive type\n        if ($data instanceof \\Illuminate\\Support\\Collection) {\n            $data = $data->all();\n        }\n\n        if (!$data || !is_array($data)) {\n            return null;\n        }\n\n        // Process multi\n        $result = [];\n\n        foreach ($data as $key => $value) {\n            $result[$key] = $processValidationValue($value);\n        }\n\n        return $result;\n    }\n\n    /**\n     * makeValidator instantiates the validator used by the validation process, depending if the\n     * class is being used inside or outside of Laravel. Optional connection string to make\n     * the validator use a different database connection than the default connection.\n     * @return \\Illuminate\\Validation\\Validator\n     */\n    protected static function makeValidator($data, $rules, $customMessages, $attributeNames, $connection = null, $verifier = null)\n    {\n        // @deprecated make required arg (v4) desired signature below\n        // makeValidator($data, $rules, $customMessages, $attributeNames, $verifier)\n        //\n        if ($verifier === null) {\n            $verifier = App::make('validation.presence');\n        }\n\n        // @deprecated set via getValidationPresenceVerifier (v4)\n        if ($connection !== null) {\n            $verifier->setConnection($connection);\n        }\n\n        $validator = Validator::make($data, $rules, $customMessages, $attributeNames);\n        $validator->setPresenceVerifier($verifier);\n\n        return $validator;\n    }\n\n    /**\n     * getValidationPresenceVerifier\n     */\n    protected function getValidationPresenceVerifier()\n    {\n        $verifier = App::make('validation.presence');\n\n        $verifier->setConnection($this->getConnectionName());\n\n        return $verifier;\n    }\n\n    /**\n     * forceSave the model even if validation fails\n     * @return bool\n     */\n    public function forceSave($options = null, $sessionKey = null)\n    {\n        return $this->saveInternal((array) $options + ['force' => true, 'sessionKey' => $sessionKey]);\n    }\n\n    /**\n     * validate the model instance\n     * @return bool\n     */\n    public function validate($rules = null, $customMessages = null, $attributeNames = null)\n    {\n        if ($this->validationErrors === null) {\n            $this->validationErrors = new MessageBag;\n        }\n\n        $throwOnValidation = property_exists($this, 'throwOnValidation')\n            ? $this->throwOnValidation\n            : true;\n\n        /**\n         * @event model.beforeValidate\n         * Called before the model is validated\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.beforeValidate', function () use (\\October\\Rain\\Database\\Model $model) {\n         *         // Prevent anything from validating ever!\n         *         return false;\n         *     });\n         *\n         */\n        if (($this->fireModelEvent('validating') === false) || ($this->fireEvent('model.beforeValidate', [], true) === false)) {\n            if ($throwOnValidation) {\n                throw new ModelException($this);\n            }\n\n            return false;\n        }\n\n        if ($this->methodExists('beforeValidate')) {\n            $this->beforeValidate();\n        }\n\n        // Perform validation\n        $rules = is_null($rules) ? $this->rules : $rules;\n        $rules = $this->processValidationRules($rules);\n        $success = true;\n\n        if (!empty($rules)) {\n            $data = $this->getValidationAttributes();\n\n            // Decode jsonable attribute values\n            foreach ($this->getJsonable() as $jsonable) {\n                $data[$jsonable] = $this->getAttribute($jsonable);\n            }\n\n            // Add relation values, if specified.\n            foreach ($rules as $attribute => $rule) {\n                if (!$this->hasRelation($attribute) || array_key_exists($attribute, $data)) {\n                    continue;\n                }\n\n                $data[$attribute] = $this->getRelationValidationValue($attribute);\n            }\n\n            // Compatibility with Hashable trait\n            // Remove all hashable values regardless, add the original values back\n            // only if they are part of the data being validated.\n            if (method_exists($this, 'getHashableAttributes')) {\n                $cleanAttributes = array_diff_key($data, array_flip($this->getHashableAttributes()));\n                $hashedAttributes = array_intersect_key($this->getOriginalHashValues(), $data);\n                $data = array_merge($cleanAttributes, $hashedAttributes);\n            }\n\n            // Compatibility with Encryptable trait\n            // Remove all encryptable values regardless, add the original values back\n            // only if they are part of the data being validated.\n            if (method_exists($this, 'getEncryptableAttributes')) {\n                $cleanAttributes = array_diff_key($data, array_flip($this->getEncryptableAttributes()));\n                $encryptedAttributes = array_intersect_key($this->getOriginalEncryptableValues(), $data);\n                $data = array_merge($cleanAttributes, $encryptedAttributes);\n            }\n\n            // Custom messages, translate internal references\n            if (property_exists($this, 'customMessages') && is_null($customMessages)) {\n                $customMessages = $this->customMessages;\n            }\n\n            if (is_null($customMessages)) {\n                $customMessages = [];\n            }\n\n            $transCustomMessages = [];\n            foreach ($customMessages as $rule => $customMessage) {\n                $transCustomMessages[$rule] = Lang::get($customMessage);\n            }\n            $customMessages = $transCustomMessages;\n\n            // Attribute names, translate internal references\n            $attrNames = (array) $this->validationDefaultAttrNames;\n\n            if (property_exists($this, 'attributeNames')) {\n                $attrNames = array_merge($attrNames, $this->attributeNames);\n            }\n\n            if ($attributeNames) {\n                $attrNames = array_merge($attrNames, (array) $attributeNames);\n            }\n\n            $transAttrNames = [];\n            foreach ($attrNames as $attribute => $attributeName) {\n                $transAttrNames[$attribute] = Lang::get($attributeName);\n            }\n            $attrNames = $transAttrNames;\n\n            // Translate any externally defined attribute names\n            $translations = Lang::get('validation.attributes');\n            if (is_array($translations)) {\n                $attrNames = array_merge($translations, $attrNames);\n            }\n\n            // Hand over to the validator\n            $validator = self::makeValidator(\n                $data,\n                $rules,\n                $customMessages,\n                $attrNames,\n                $this->getConnectionName(),\n                $this->getValidationPresenceVerifier()\n            );\n\n            $success = $validator->passes();\n\n            if ($success) {\n                if ($this->validationErrors->count() > 0) {\n                    $this->validationErrors = new MessageBag;\n                }\n            }\n            else {\n                $this->validationErrors = $validator->messages();\n                if (Input::hasSession()) {\n                    Input::flash();\n                }\n            }\n        }\n\n        /**\n         * @event model.afterValidate\n         * Called after the model is validated\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.afterValidate', function () use (\\October\\Rain\\Database\\Model $model) {\n         *         \\Log::info(\"{$model->name} successfully passed validation\");\n         *     });\n         *\n         */\n        $this->fireModelEvent('validated', false);\n        $this->fireEvent('model.afterValidate');\n\n        if ($this->methodExists('afterValidate')) {\n            $this->afterValidate();\n        }\n\n        if (!$success && $throwOnValidation) {\n            throw new ModelException($this);\n        }\n\n        return $success;\n    }\n\n    /**\n     * processValidationRules\n     */\n    protected function processValidationRules($rules)\n    {\n        // Run through field names and convert array notation field names to dot notation\n        $rules = $this->processRuleFieldNames($rules);\n\n        foreach ($rules as $field => $ruleParts) {\n            // Trim empty rules\n            if (is_string($ruleParts) && trim($ruleParts) === '') {\n                unset($rules[$field]);\n                continue;\n            }\n\n            // Normalize rule sets\n            if (!is_array($ruleParts)) {\n                $ruleParts = explode('|', $ruleParts);\n            }\n\n            // Analyze each rule individually\n            foreach ($ruleParts as $key => $rulePart) {\n                // Allow rule objects\n                if (is_object($rulePart)) {\n                    continue;\n                }\n                // Remove primary key unique validation rule if the model already exists\n                if (str_starts_with($rulePart, 'unique')) {\n                    $ruleParts[$key] = $this->processValidationUniqueRule($rulePart, $field);\n                }\n                // Look for required:create and required:update rules\n                elseif (str_starts_with($rulePart, 'required:create') && $this->exists) {\n                    unset($ruleParts[$key]);\n                }\n                elseif (str_starts_with($rulePart, 'required:update') && !$this->exists) {\n                    unset($ruleParts[$key]);\n                }\n            }\n\n            $rules[$field] = $ruleParts;\n        }\n\n        return $rules;\n    }\n\n    /**\n     * processRuleFieldNames processes field names in a rule array\n     * Converts any field names using array notation (ie. `field[child]`) into dot notation (ie. `field.child`)\n     * @param array $rules Rules array\n     * @return array\n     */\n    protected function processRuleFieldNames($rules)\n    {\n        $processed = [];\n\n        foreach ($rules as $field => $ruleParts) {\n            $fieldName = $field;\n\n            if (preg_match('/^.*?\\[.*?\\]/', $fieldName)) {\n                $fieldName = str_replace('[]', '.*', $fieldName);\n                $fieldName = str_replace(['[', ']'], ['.', ''], $fieldName);\n            }\n\n            $processed[$fieldName] = $ruleParts;\n        }\n\n        return $processed;\n    }\n\n    /**\n     * processValidationUniqueRule rebuilds the unique validation rule to force for the existing key\n     * exclusion for existing models. It also checks for unique rules without a table name and includes\n     * the table name, since this is required by Laravel but not October.\n     * @param string $definition\n     * @param string $fieldName\n     * @return string\n     */\n    protected function processValidationUniqueRule($definition, $fieldName)\n    {\n        if (!$this->exists) {\n            if ($definition === 'unique' || $definition === 'unique_site') {\n                return $definition . ':' . $this->getTable();\n            }\n            return $definition;\n        }\n\n        [$ruleName, $ruleDefinition] = array_pad(explode(':', $definition, 2), 2, '');\n        [$tableName, $column, $key, $keyName, $whereColumn, $whereValue] = array_pad(explode(',', $ruleDefinition, 6), 6, null);\n\n        $tableName = $tableName ?: $this->getTable();\n        $column = $column ?: $fieldName;\n        $key = $keyName ? $this->$keyName : $this->getKey();\n        $keyName = $keyName ?: $this->getKeyName();\n\n        $params = [$tableName, $column, $key, $keyName];\n\n        if ($whereColumn) {\n            $params[] = $whereColumn;\n        }\n\n        if ($whereValue) {\n            $params[] = $whereValue;\n        }\n\n        return $ruleName . ':' . implode(',', $params);\n    }\n\n    /**\n     * isAttributeRequired determines if an attribute is required based on the validation rules.\n     * checkDependencies checks the attribute dependencies (for required_if & required_with rules).\n     * Note that it will only be checked up to the next level, if another dependent rule is found\n     * then it will just assume the field is required.\n     * @param  string  $attribute\n     * @param bool $checkDependencies\n     * @return bool\n     */\n    public function isAttributeRequired($attribute, $checkDependencies = true)\n    {\n        if (!isset($this->rules[$attribute])) {\n            return false;\n        }\n\n        $ruleSet = $this->rules[$attribute];\n\n        if (is_array($ruleSet)) {\n            $ruleSet = implode('|', $ruleSet);\n        }\n\n        if (strpos($ruleSet, 'required:create') !== false && $this->exists) {\n            return false;\n        }\n\n        if (strpos($ruleSet, 'required:update') !== false && !$this->exists) {\n            return false;\n        }\n\n        if (strpos($ruleSet, 'required_with') !== false) {\n            if (!$checkDependencies) {\n                return true;\n            }\n\n            $requiredWith = substr($ruleSet, strpos($ruleSet, 'required_with') + 14);\n\n            if (strpos($requiredWith, '|') !== false) {\n                $requiredWith = substr($requiredWith, 0, strpos($requiredWith, '|'));\n            }\n\n            return $this->isAttributeRequired($requiredWith, false);\n        }\n\n        if (strpos($ruleSet, 'required_if') !== false) {\n            if (!$checkDependencies) {\n                return true;\n            }\n\n            $requiredIf = substr($ruleSet, strpos($ruleSet, 'required_if') + 12);\n            $requiredIf = substr($requiredIf, 0, strpos($requiredIf, ','));\n\n            return $this->isAttributeRequired($requiredIf, false);\n        }\n\n        return strpos($ruleSet, 'required') !== false;\n    }\n\n    /**\n     * errors gets validation error message collection for the Model\n     * @return \\Illuminate\\Support\\MessageBag\n     */\n    public function errors()\n    {\n        return $this->validationErrors;\n    }\n\n    /**\n     * validating creates a new native event for handling beforeValidate()\n     * @param Closure|string $callback\n     * @return void\n     */\n    public static function validating($callback)\n    {\n        static::registerModelEvent('validating', $callback);\n    }\n\n    /**\n     * validated create a new native event for handling afterValidate()\n     * @param Closure|string $callback\n     * @return void\n     */\n    public static function validated($callback)\n    {\n        static::registerModelEvent('validated', $callback);\n    }\n}\n"
  },
  {
    "path": "src/Database/TreeCollection.php",
    "content": "<?php namespace October\\Rain\\Database;\n\n/**\n * TreeCollection is a custom collection used by NestedTree trait.\n *\n * General access methods:\n *\n *   $collection->toNested(); // Converts collection to an eager loaded one.\n *\n */\nclass TreeCollection extends Collection\n{\n    /**\n     * toNested converts a flat collection of nested set models to an set where\n     * children is eager loaded. removeOrphans removes nodes that exist without\n     * their parents.\n     * @param bool $removeOrphans\n     * @return \\October\\Rain\\Database\\Collection\n     */\n    public function toNested($removeOrphans = true)\n    {\n        // Multisite\n        $keyMethod = 'getKey';\n        if (\n            ($model = $this->first()) &&\n            $model->isClassInstanceOf(\\October\\Contracts\\Database\\MultisiteInterface::class) &&\n            $model->isAttributePropagatable('children') &&\n            $model->isAttributePropagatable('parent')\n        ) {\n            $keyMethod = 'getMultisiteKey';\n        }\n\n        // Get dictionary\n        $collection = [];\n        foreach ($this as $item) {\n            $collection[$item->{$keyMethod}()] = $item;\n        }\n\n        // Set new collection for \"children\" relations\n        foreach ($collection as $key => $model) {\n            $model->setRelation('children', new Collection);\n        }\n\n        // Assign all child nodes to their parents\n        $nestedKeys = [];\n        foreach ($collection as $key => $model) {\n            if (!$parentKey = $model->getParentId()) {\n                continue;\n            }\n\n            if (array_key_exists($parentKey, $collection)) {\n                $collection[$parentKey]->children[] = $model;\n                $nestedKeys[] = $model->{$keyMethod}();\n            }\n            elseif ($removeOrphans) {\n                $nestedKeys[] = $model->{$keyMethod}();\n            }\n        }\n\n        // Remove processed nodes\n        foreach ($nestedKeys as $key) {\n            unset($collection[$key]);\n        }\n\n        return new Collection($collection);\n    }\n\n    /**\n     * listsNested gets an array with values of a given column. Values are indented according\n     * to their depth.\n     * @param  string $value  Array values\n     * @param  string $key    Array keys\n     * @param  string $indent Character to indent depth\n     * @return array\n     */\n    public function listsNested($value, $key = null, $indent = '&nbsp;&nbsp;&nbsp;')\n    {\n        // Recursive helper function\n        $buildCollection = function ($items, $depth = 0) use (&$buildCollection, $value, $key, $indent) {\n            $result = [];\n\n            $indentString = str_repeat($indent, $depth);\n\n            foreach ($items as $item) {\n                if ($key !== null) {\n                    $result[$item->{$key}] = $indentString . $item->{$value};\n                }\n                else {\n                    $result[] = $indentString . $item->{$value};\n                }\n\n                // Add the children\n                $childItems = $item->getChildren();\n                if ($childItems->count() > 0) {\n                    $result = $result + $buildCollection($childItems, $depth + 1);\n                }\n            }\n\n            return $result;\n        };\n\n        // Build a nested collection\n        $rootItems = $this->toNested();\n        return $buildCollection($rootItems);\n    }\n}\n"
  },
  {
    "path": "src/Database/Updater.php",
    "content": "<?php namespace October\\Rain\\Database;\n\nuse Model;\nuse Exception;\nuse ReflectionClass;\n\n/**\n * Updater executes database migration and seed scripts based on their filename.\n *\n * @package october\\database\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Updater\n{\n    /**\n     * @var bool skippingErrors determines if exceptions should be thrown\n     */\n    protected static $skippingErrors = false;\n\n    /**\n     * @var array requiredPathCache paths that have already been required.\n     */\n    protected static $requiredPathCache = [];\n\n    /**\n     * skipErrors will continue through exceptions\n     * @param  bool  $state\n     */\n    public static function skipErrors($state = true)\n    {\n        static::$skippingErrors = $state;\n    }\n\n    /**\n     * setUp a migration or seed file.\n     */\n    public function setUp($file)\n    {\n        $object = $this->resolve($file);\n\n        if ($object === null) {\n            return false;\n        }\n\n        $this->isValidScript($object);\n\n        Model::unguard();\n\n        if ($object instanceof Updates\\Migration) {\n            $this->runMethod($object, 'up');\n        }\n        elseif ($object instanceof Updates\\Seeder) {\n            $this->runMethod($object, 'run');\n        }\n\n        Model::reguard();\n\n        return true;\n    }\n\n    /**\n     * packDown a migration or seed file.\n     */\n    public function packDown($file)\n    {\n        $object = $this->resolve($file);\n\n        if ($object === null) {\n            return false;\n        }\n\n        $this->isValidScript($object);\n\n        Model::unguard();\n\n        if ($object instanceof Updates\\Migration) {\n            $this->runMethod($object, 'down');\n        }\n\n        Model::reguard();\n\n        return true;\n    }\n\n    /**\n     * resolve a migration instance from a file.\n     * @param  string  $file\n     * @return object\n     */\n    public function resolve(string $path)\n    {\n        if (!is_file($path)) {\n            return;\n        }\n\n        $class = $this->getClassFromFile($path);\n        if (class_exists($class) && realpath($path) == (new ReflectionClass($class))->getFileName()) {\n            return new $class;\n        }\n\n        $migration = static::$requiredPathCache[$path] ??= require $path;\n        if (is_object($migration)) {\n            return method_exists($migration, '__construct')\n                ? require $path\n                : clone $migration;\n        }\n\n        if (str_ends_with($class, 'class@anonymous')) {\n            throw new Exception(\"Anonymous class in [{$path}] could not be resolved\");\n        }\n\n        return new $class;\n    }\n\n    /**\n     * runMethod on a migration or seed\n     */\n    protected function runMethod($migration, $method)\n    {\n        try {\n            $migration->{$method}();\n        }\n        catch (Exception $ex) {\n            if (!static::$skippingErrors) {\n                throw $ex;\n            }\n        }\n    }\n\n    /**\n     * isValidScript checks if the object is a valid update script.\n     */\n    protected function isValidScript($object)\n    {\n        if ($object instanceof Updates\\Migration) {\n            return true;\n        }\n        elseif ($object instanceof Updates\\Seeder) {\n            return true;\n        }\n\n        throw new Exception(sprintf(\n            'Database script [%s] must inherit October\\Rain\\Database\\Updates\\Migration or October\\Rain\\Database\\Updates\\Seeder classes',\n            get_class($object)\n        ));\n    }\n\n    /**\n     * getClassFromFile extracts the namespace and class name from a file.\n     * @param string $file\n     * @return string\n     */\n    public function getClassFromFile($file)\n    {\n        $fileParser = fopen($file, 'r');\n        $class = $namespace = $buffer = '';\n        $i = 0;\n\n        while (!$class) {\n            if (feof($fileParser)) {\n                break;\n            }\n\n            $buffer .= fread($fileParser, 512);\n\n            // Prefix and suffix string to prevent unterminated comment warning\n            $tokens = token_get_all('/**/' . $buffer . '/**/');\n\n            if (strpos($buffer, '{') === false) {\n                continue;\n            }\n\n            for (; $i < count($tokens); $i++) {\n                // Namespace opening\n                if ($tokens[$i][0] === T_NAMESPACE) {\n                    for ($j = $i + 1; $j < count($tokens); $j++) {\n                        if ($tokens[$j] === ';') {\n                            break;\n                        }\n\n                        $namespace .= is_array($tokens[$j]) ? $tokens[$j][1] : $tokens[$j];\n                    }\n                }\n\n                // Class opening\n                if ($tokens[$i][0] === T_CLASS && $tokens[$i-1][1] !== '::') {\n                    // Anonymous Class\n                    if ($tokens[$i-2][0] === T_NEW && $tokens[$i-4][0] === T_RETURN) {\n                        $class = 'class@anonymous';\n                        break;\n                    }\n\n                    $class = $tokens[$i+2][1];\n                    break;\n                }\n            }\n        }\n\n        if (!strlen(trim($namespace)) && !strlen(trim($class))) {\n            return false;\n        }\n\n        return trim($namespace) . '\\\\' . trim($class);\n    }\n}\n"
  },
  {
    "path": "src/Database/Updates/Migration.php",
    "content": "<?php namespace October\\Rain\\Database\\Updates;\n\nuse Illuminate\\Database\\Migrations\\Migration as MigrationBase;\n\nclass Migration extends MigrationBase\n{\n}\n"
  },
  {
    "path": "src/Database/Updates/Seeder.php",
    "content": "<?php namespace October\\Rain\\Database\\Updates;\n\nuse Illuminate\\Database\\Seeder as SeederBase;\n\n/**\n * Seeder\n */\nclass Seeder extends SeederBase\n{\n    /**\n     * line writes a string as standard output.\n     * @param  string  $string\n     * @param  string|null  $style\n     * @return void\n     */\n    public function line($string, $style = null)\n    {\n        if (!isset($this->command)) {\n            return;\n        }\n\n        $styled = $style ? \"<$style>$string</$style>\" : $string;\n\n        $this->command->getOutput()->writeln($styled);\n    }\n}\n"
  },
  {
    "path": "src/Element/Dash/ReportDefinition.php",
    "content": "<?php namespace October\\Rain\\Element\\Dash;\n\nuse October\\Rain\\Element\\ElementBase;\n\n/**\n * ReportDefinition\n *\n * @method ReportDefinition reportName(string $name) reportName for this report\n * @method ReportDefinition label(string $label) label for this report\n * @method ReportDefinition type(string $type) type for display mode, eg: indicator, static\n * @method ReportDefinition row(int $row) row number where the report should be placed\n * @method ReportDefinition width(int $width) width to display the report, between 1 - 20 range\n * @method ReportDefinition icon(string $icon) icon specifies an icon name for this report\n * @method ReportDefinition dimension(string $dimension) dimension name\n * @method ReportDefinition dataSource(string $dataSource) dataSource class name for obtaining report data\n * @method ReportDefinition widget(string $widget) widget code or class name for the report widget\n * @method ReportDefinition metrics(array $metrics) metrics to display with the report\n * @method ReportDefinition dateStart(string $dateStart) dateStart\n * @method ReportDefinition dateEnd(string $dateEnd) dateEnd\n * @method ReportDefinition compareWith(string $compareWith) compareWith period, eg: prev-period, prev-year\n * @method ReportDefinition resetCache(bool $resetCache) resetCache when rendering\n * @method ReportDefinition aggregationInterval(string $aggregationInterval) aggregationInterval for display, eg: day, week, month\n *\n * @package october\\element\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ReportDefinition extends ElementBase\n{\n    /**\n     * initDefaultValues for this report\n     */\n    protected function initDefaultValues()\n    {\n        $this\n            ->displayAs('static')\n            ->metrics([])\n            ->width(20)\n            ->row(1)\n        ;\n    }\n\n    /**\n     * displayAs type for this field\n     */\n    public function displayAs(string $type): ReportDefinition\n    {\n        $this->type($type);\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Element/ElementBase.php",
    "content": "<?php namespace October\\Rain\\Element;\n\nuse Illuminate\\Contracts\\Support\\Arrayable;\nuse Illuminate\\Contracts\\Support\\Jsonable;\nuse October\\Rain\\Extension\\Extendable;\nuse JsonSerializable;\nuse ArrayAccess;\n\n/**\n * ElementBase class for all elements\n *\n * @package october\\element\n * @author Alexey Bobkov, Samuel Georges\n */\nabstract class ElementBase extends Extendable implements Arrayable, ArrayAccess, Jsonable, JsonSerializable\n{\n    /**\n     * @var array config values for this instance\n     */\n    public $config = [];\n\n    /**\n     * __construct\n     */\n    public function __construct($config = [])\n    {\n        $this->initDefaultValues();\n\n        $this->useConfig($config);\n    }\n\n    /**\n     * initDefaultValues override method\n     */\n    protected function initDefaultValues()\n    {\n    }\n\n    /**\n     * evalConfig override method\n     */\n    public function evalConfig(array $config)\n    {\n    }\n\n    /**\n     * useConfig is used internally\n     */\n    public function useConfig(array $config): ElementBase\n    {\n        $this->config = array_merge($this->config, $config);\n\n        $this->evalConfig($config);\n\n        return $this;\n    }\n\n    /**\n     * getConfig returns the entire config array\n     */\n    public function getConfig($key = null, $default = null)\n    {\n        if ($key !== null) {\n            return $this->get($key, $default);\n        }\n\n        return $this->config;\n    }\n\n    /**\n     * get an attribute from the element instance.\n     * @param  string  $key\n     */\n    public function get($key, $default = null)\n    {\n        if (array_key_exists($key, $this->config)) {\n            return $this->config[$key];\n        }\n\n        return value($default);\n    }\n\n    /**\n     * toArray converts the element instance to an array.\n     * @return array\n     */\n    public function toArray()\n    {\n        return $this->config;\n    }\n\n    /**\n     * jsonSerialize converts the object into something JSON serializable.\n     */\n    public function jsonSerialize(): array\n    {\n        return $this->toArray();\n    }\n\n    /**\n     * toJson converts the element instance to JSON.\n     * @param  int  $options\n     * @return string\n     */\n    public function toJson($options = 0)\n    {\n        return json_encode($this->jsonSerialize(), $options);\n    }\n\n    /**\n     * offsetExists determines if the given offset exists.\n     * @param  string  $offset\n     * @return bool\n     */\n    public function offsetExists($offset): bool\n    {\n        return isset($this->config[$offset]);\n    }\n\n    /**\n     * offsetGet gets the value for a given offset.\n     * @param  string  $offset\n     * @return mixed\n     */\n    public function offsetGet($offset): mixed\n    {\n        return $this->get($offset);\n    }\n\n    /**\n     * offsetSet sets the value at the given offset.\n     * @param  string  $offset\n     * @param  mixed  $value\n     */\n    public function offsetSet($offset, $value): void\n    {\n        $this->config[$offset] = $value;\n    }\n\n    /**\n     * offsetUnset unsets the value at the given offset.\n     * @param  string  $offset\n     * @return void\n     */\n    public function offsetUnset($offset): void\n    {\n        unset($this->config[$offset]);\n    }\n\n    /**\n     * __call handles dynamic calls to the element instance to set config.\n     * @param  string  $method\n     * @param  array  $parameters\n     * @return $this\n     */\n    public function __call($method, $parameters)\n    {\n        $this->config[$method] = count($parameters) > 0 ? $parameters[0] : true;\n\n        return $this;\n    }\n\n    /**\n     * __get dynamically retrieves the value of an attribute.\n     * @param  string  $key\n     * @return mixed\n     */\n    public function __get($key)\n    {\n        return $this->get($key);\n    }\n\n    /**\n     * __set dynamically sets the value of an attribute.\n     *\n     * @param  string  $key\n     * @param  mixed  $value\n     * @return void\n     */\n    public function __set($key, $value)\n    {\n        $this->offsetSet($key, $value);\n    }\n\n    /**\n     * __isset dynamically checks if an attribute is set.\n     *\n     * @param  string  $key\n     * @return bool\n     */\n    public function __isset($key)\n    {\n        return $this->offsetExists($key);\n    }\n\n    /**\n     * __unset dynamically unsets an attribute.\n     *\n     * @param  string  $key\n     * @return void\n     */\n    public function __unset($key)\n    {\n        $this->offsetUnset($key);\n    }\n}\n"
  },
  {
    "path": "src/Element/ElementHolder.php",
    "content": "<?php namespace October\\Rain\\Element;\n\nuse October\\Rain\\Support\\Collection;\nuse IteratorAggregate;\nuse Traversable;\n\n/**\n * ElementHolder is a general collection used when passing a group of elements by reference,\n * allowing access via array and object notation.\n *\n * @package october\\element\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ElementHolder extends ElementBase implements IteratorAggregate\n{\n    /**\n     * @var array touchedElements is used by getTouchedElements\n     */\n    protected $touchedElements = [];\n\n    /**\n     * getTouchedElements return field names that have been accessed\n     */\n    public function getTouchedElements(): array\n    {\n        return $this->touchedElements;\n    }\n\n    /**\n     * get an element from the holder instance.\n     * @param  string  $key\n     * @param  mixed  $default\n     * @return mixed\n     */\n    public function get($key, $default = null)\n    {\n        if (isset($this->touchedElements[$key])) {\n            return $this->touchedElements[$key];\n        }\n\n        if (isset($this->config[$key])) {\n            return $this->touchedElements[$key] = $this->config[$key];\n        }\n\n        return parent::get($key, $default);\n    }\n\n    /**\n     * getIterator for the elements.\n     */\n    public function getIterator(): Traversable\n    {\n        return new Collection($this->config);\n    }\n}\n"
  },
  {
    "path": "src/Element/Filter/ScopeDefinition.php",
    "content": "<?php namespace October\\Rain\\Element\\Filter;\n\nuse October\\Rain\\Element\\ElementBase;\nuse October\\Rain\\Element\\OptionDefinition;\n\n/**\n * ScopeDefinition\n *\n * @method ScopeDefinition useConfig(array $config) useConfig applies the supplied configuration\n * @method ScopeDefinition scopeName(string $name) scopeName for this scope\n * @method ScopeDefinition label(string $label) label for this scope\n * @method ScopeDefinition shortLabel(string $shortLabel) shortLabel used in list headers\n * @method ScopeDefinition value(mixed $value) current value for this scope\n * @method ScopeDefinition nameFrom(string $column) nameFrom model attribute to use for the display name\n * @method ScopeDefinition valueFrom(mixed $value) valueFrom model attribute to use for the source value\n * @method ScopeDefinition descriptionFrom(string $column) descriptionFrom column to use for the description\n * @method ScopeDefinition options(mixed $options) options for the scope\n * @method ScopeDefinition dependsOn(array $scopes) dependsOn other scopes, when the other scopes are modified, this scope will update\n * @method ScopeDefinition context(string $context) context visibility of this scope\n * @method ScopeDefinition defaults(mixed $value) default value for the scope\n * @method ScopeDefinition conditions(string $sql) conditions to apply in raw SQL format\n * @method ScopeDefinition scope(string $method) scope method for the model\n * @method ScopeDefinition cssClass(string $class) cssClass to attach to the scope container\n * @method ScopeDefinition emptyOption(string $label) emptyOption label for intentionally selecting an empty value (optional)\n * @method ScopeDefinition disabled(bool $value) disabled setting for the scope\n * @method ScopeDefinition order(int $order) order number when displaying\n * @method ScopeDefinition after(string $after) after places this scope after another existing scope name using the display order (+1)\n * @method ScopeDefinition before(string $before) before places this scope before another existing scope name using the display order (-1)\n *\n * @package october\\element\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ScopeDefinition extends ElementBase\n{\n    /**\n     * initDefaultValues for this scope\n     */\n    protected function initDefaultValues()\n    {\n        $this\n            ->displayAs('group')\n            ->nameFrom('name')\n            ->disabled(false)\n            ->order(-1)\n        ;\n    }\n\n    /**\n     * useConfig\n     */\n    public function useConfig(array $config): ElementBase\n    {\n        parent::useConfig($config);\n\n        // The config default proxies to defaults\n        if (array_key_exists('default', $this->config)) {\n            $this->defaults($this->config['default']);\n        }\n\n        return $this;\n    }\n\n    /**\n     * displayAs type for this scope. Supported modes are:\n     * - group - filter by a group of IDs. Default.\n     * - checkbox - filter by a simple toggle switch.\n     */\n    public function displayAs(string $type): ScopeDefinition\n    {\n        $this->type($type);\n\n        return $this;\n    }\n\n    /**\n     * hasOptions returns true if options have been specified\n     */\n    public function hasOptions(): bool\n    {\n        return $this->options !== null &&\n            (is_array($this->options) || is_callable($this->options));\n    }\n\n    /**\n     * options get/set for dropdowns, radio lists and checkbox lists\n     * @return array|self\n     */\n    public function options($value = null)\n    {\n        if ($value === null) {\n            if (is_array($this->options)) {\n                return $this->options;\n            }\n\n            if (is_callable($this->options)) {\n                $callable = $this->options;\n                return $callable();\n            }\n\n            return [];\n        }\n\n        $this->config['options'] = $value;\n\n        return $this;\n    }\n\n    /**\n     * optionsDefinition\n     */\n    public function asOptionsDefinition($options = null)\n    {\n        if ($options === null) {\n            $options = $this->options();\n        }\n\n        $result = [];\n\n        foreach ($options as $value => $option) {\n            $result[$value] = (new OptionDefinition)->useOptionConfig($value, $option);\n        }\n\n        return $result;\n    }\n\n    /**\n     * setScopeValue and merge the values as config\n     */\n    public function setScopeValue($value)\n    {\n        if (is_array($value)) {\n            $this->config = array_merge($this->config, $value);\n        }\n\n        $this->scopeValue($value);\n    }\n}\n"
  },
  {
    "path": "src/Element/Form/FieldDefinition.php",
    "content": "<?php namespace October\\Rain\\Element\\Form;\n\nuse October\\Rain\\Element\\ElementBase;\nuse October\\Rain\\Element\\OptionDefinition;\n\n/**\n * FieldDefinition\n *\n * @method FieldDefinition useConfig(array $config) useConfig applies the supplied configuration\n * @method FieldDefinition fieldName(string $name) fieldName for this field\n * @method FieldDefinition label(string $label) label for this field\n * @method FieldDefinition value(string $value) value for the form field\n * @method FieldDefinition valueFrom(string $valueFrom) valueFrom model attribute to use for the display value.\n * @method FieldDefinition defaults(string $defaults) defaults specifies a default value for supported fields.\n * @method FieldDefinition defaultFrom(string $defaultFrom) defaultFrom model attribute to use for the default value.\n * @method FieldDefinition type(string $type) type for display mode, eg: text, textarea\n * @method FieldDefinition autoFocus(bool $autoFocus) autoFocus flags the field to be focused on load.\n * @method FieldDefinition readOnly(bool $readOnly) readOnly specifies if the field is read-only or not.\n * @method FieldDefinition disabled(bool $disabled) disabled specifies if the field is disabled or not.\n * @method FieldDefinition hidden(bool $hidden) hidden defines the field without ever displaying it\n * @method FieldDefinition tab(string $tab) tab this field belongs to\n * @method FieldDefinition span(string $span, string $spanClass) span specifies the field size and side, eg: auto, left, right, full\n * @method FieldDefinition spanClass(string $spanClass) spanClass is used by the row span type for a custom css class\n * @method FieldDefinition size(string $size) size for the field, eg: tiny, small, large, huge, giant\n * @method FieldDefinition options(array|callable $options) options available\n * @method FieldDefinition comment(string $comment) comment for the form field\n * @method FieldDefinition commentAbove(string $comment) commentAbove the form field\n * @method FieldDefinition commentHtml(bool $commentHtml) commentHtml if the comment is in HTML format\n * @method FieldDefinition placeholder(string $placeholder) placeholder to display when there is no value supplied\n * @method FieldDefinition order(int $order) order number when displaying\n *\n * @package october\\element\n * @author Alexey Bobkov, Samuel Georges\n */\nclass FieldDefinition extends ElementBase\n{\n    /**\n     * @var callable optionsCallback\n     */\n    protected $optionsCallback;\n\n    /**\n     * initDefaultValues for this field\n     */\n    protected function initDefaultValues()\n    {\n        $this\n            ->hidden(false)\n            ->autoFocus(false)\n            ->readOnly(false)\n            ->disabled(false)\n            ->displayAs('text')\n            ->span('full')\n            ->size('large')\n            ->commentPosition('below')\n            ->commentHtml(false)\n            ->spanClass('')\n            ->comment('')\n            ->placeholder('')\n            ->order(-1)\n        ;\n    }\n\n    /**\n     * useConfig\n     */\n    public function useConfig(array $config): ElementBase\n    {\n        parent::useConfig($config);\n\n        // The config default proxies to defaults\n        if (array_key_exists('default', $this->config)) {\n            $this->defaults($this->config['default']);\n        }\n\n        return $this;\n    }\n\n    /**\n     * displayAs type for this field\n     */\n    public function displayAs(string $type): FieldDefinition\n    {\n        $this->type($type);\n\n        return $this;\n    }\n\n    /**\n     * span sets a side of the field on a form\n     */\n    public function span(string $value = 'full', string $spanClass = ''): FieldDefinition\n    {\n        $this->span = $value;\n\n        $this->spanClass = $spanClass;\n\n        return $this;\n    }\n\n    /**\n     * hasOptions returns true if options have been specified\n     */\n    public function hasOptions(): bool\n    {\n        if ($this->optionsCallback !== null) {\n            return true;\n        }\n\n        if ($this->options !== null && is_array($this->options)) {\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * options get/set for dropdowns, radio lists and checkbox lists\n     * @return array|self\n     */\n    public function options($value = null)\n    {\n        // get\n        if ($value === null) {\n            if ($this->optionsCallback !== null) {\n                $callable = $this->optionsCallback;\n                return $callable();\n            }\n\n            if (is_array($this->options)) {\n                return $this->options;\n            }\n\n            return [];\n        }\n\n        // set\n        if (is_callable($value)) {\n            $this->optionsCallback = $value;\n        }\n        else {\n            $this->options = $value;\n        }\n\n        return $this;\n    }\n\n    /**\n     * optionsDefinition\n     */\n    public function asOptionsDefinition($options = null)\n    {\n        if ($options === null) {\n            $options = $this->options();\n        }\n\n        $result = [];\n\n        foreach ($options as $value => $option) {\n            $result[$value] = (new OptionDefinition)->useOptionConfig($value, $option);\n        }\n\n        return $result;\n    }\n\n    /**\n     * matchesContext returns true if the field matches the supplied context\n     */\n    public function matchesContext($context): bool\n    {\n        if ($context === '*' || $this->context === null) {\n            return true;\n        }\n\n        return in_array($context, (array) $this->context);\n    }\n}\n"
  },
  {
    "path": "src/Element/Form/FieldsetDefinition.php",
    "content": "<?php namespace October\\Rain\\Element\\Form;\n\nuse October\\Rain\\Element\\ElementBase;\nuse IteratorAggregate;\nuse ArrayIterator;\n\n/**\n * FieldsetDefinition\n *\n * @method FieldsetDefinition defaultTab(string $defaultTab) defaultTab is default tab label to use when none is specified\n * @method FieldsetDefinition suppressTabs(bool $suppressTabs) suppressTabs if set to TRUE, fields will not be displayed in tabs\n *\n * @package october\\element\n * @author Alexey Bobkov, Samuel Georges\n */\nclass FieldsetDefinition extends ElementBase implements IteratorAggregate\n{\n    /**\n     * @var array fields is a collection of panes fields to these tabs\n     */\n    protected $fields = [];\n\n    /**\n     * initDefaultValues for this scope\n     */\n    protected function initDefaultValues()\n    {\n        $this\n            ->defaultTab('Misc')\n            ->suppressTabs(false)\n        ;\n    }\n\n    /**\n     * addField to the collection of tabs\n     */\n    public function addField($name, FieldDefinition $field)\n    {\n        $this->fields[$name] = $field;\n    }\n\n    /**\n     * removeField from all tabs by name\n     * @param string $name\n     * @return boolean\n     */\n    public function removeField($name)\n    {\n        if (isset($this->fields[$name])) {\n            unset($this->fields[$name]);\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * hasFields returns true if any fields have been registered for these tabs\n     * @return bool\n     */\n    public function hasFields()\n    {\n        return count($this->fields) > 0;\n    }\n\n    /**\n     * getFields returns an array of the registered fields, includes tabs in format\n     * array[tab][field]\n     * @return array\n     */\n    public function getFields()\n    {\n        $fieldsTabbed = [];\n\n        foreach ($this->fields as $name => $field) {\n            $tabName = $field->tab ?: $this->defaultTab;\n            $fieldsTabbed[$tabName][$name] = $field;\n        }\n\n        return $fieldsTabbed;\n    }\n\n    /**\n     * getField object specified\n     */\n    public function getField(string $field)\n    {\n        if (isset($this->fields[$field])) {\n            return $this->fields[$field];\n        }\n\n        return null;\n    }\n\n    /**\n     * getAllFields returns an array of the registered fields, without tabs\n     * @return array\n     */\n    public function getAllFields()\n    {\n        return $this->fields;\n    }\n\n    /**\n     * sortAllFields will sort the defined fields by their order attribute\n     */\n    public function sortAllFields()\n    {\n        uasort($this->fields, static function ($a, $b) {\n            return $a->order - $b->order;\n        });\n    }\n\n    /**\n     * getIterator gets an iterator for the items\n     * @return ArrayIterator\n     */\n    public function getIterator(): ArrayIterator\n    {\n        return new ArrayIterator(\n            $this->suppressTabs\n                ? $this->getAllFields()\n                : $this->getFields()\n        );\n    }\n}\n"
  },
  {
    "path": "src/Element/Lists/ColumnDefinition.php",
    "content": "<?php namespace October\\Rain\\Element\\Lists;\n\nuse October\\Rain\\Element\\ElementBase;\n\n/**\n * ColumnDefinition\n *\n * @method ColumnDefinition useConfig(array $config) useConfig applies the supplied configuration\n * @method ColumnDefinition columnName(string $name) columnName for this column\n * @method ColumnDefinition label(string $label) label for list column\n * @method ColumnDefinition shortLabel(string $shortLabel) shortLabel used in list headers\n * @method ColumnDefinition type(string $type) type for display mode, eg: text, number\n * @method ColumnDefinition align(string $align) align the column, eg: left, right or center\n * @method ColumnDefinition hidden(bool $hidden) hidden defines the column without ever displaying it\n * @method ColumnDefinition sortable(bool $sortable) sortable specifies if this column can be sorted\n * @method ColumnDefinition searchable(bool $searchable) searchable specifies if this column can be searched\n * @method ColumnDefinition invisible(bool $invisible) invisible is hidden in default list settings\n * @method ColumnDefinition clickable(bool $clickable) clickable disables the default click behavior when the column is clicked\n * @method ColumnDefinition order(int $order) order number when displaying\n * @method ColumnDefinition after(string $after) after places this column after another existing column name using the display order (+1)\n * @method ColumnDefinition before(string $before) before places this column before another existing column name using the display order (-1)\n *\n * @package october\\element\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ColumnDefinition extends ElementBase\n{\n    /**\n     * initDefaultValues for this column\n     */\n    protected function initDefaultValues()\n    {\n        $this\n            ->displayAs('text')\n            ->hidden(false)\n            ->sortable()\n            ->searchable(false)\n            ->invisible(false)\n            ->clickable()\n            ->order(-1)\n        ;\n    }\n\n    /**\n     * displayAs type for this column\n     * @todo $config is deprecated, see useConfig\n     */\n    public function displayAs(string $type): ColumnDefinition\n    {\n        $this->type = $type;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Element/Navigation/ItemDefinition.php",
    "content": "<?php namespace October\\Rain\\Element\\Navigation;\n\nuse October\\Rain\\Element\\ElementBase;\n\n/**\n * ItemDefinition\n *\n * @method ItemDefinition useConfig(array $config) useConfig applies the supplied configuration\n * @method ItemDefinition code(string $code) code for the nav item\n * @method ItemDefinition label(string $label) label for the nav item\n * @method ItemDefinition url(string $url) url address for the nav item\n * @method ItemDefinition icon(null $icon) icon to display\n * @method ItemDefinition order(int $order) order number when displaying\n * @method ItemDefinition customData(array $customData) customData to include with the nav item\n *\n * @package october\\element\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ItemDefinition extends ElementBase\n{\n    /**\n     * initDefaultValues for this item\n     */\n    protected function initDefaultValues()\n    {\n        $this\n            ->order(-1)\n        ;\n    }\n}\n"
  },
  {
    "path": "src/Element/OptionDefinition.php",
    "content": "<?php namespace October\\Rain\\Element;\n\nuse Arr;\nuse Html;\nuse October\\Rain\\Element\\ElementBase;\n\n/**\n * OptionDefinition represents a single option that can be associated to an field field\n *\n * @link https://docs.octobercms.com/3.x/element/define-options.html\n *\n * @method OptionDefinition label(string $label) label for this option\n * @method OptionDefinition comment(string $comment) comment for the form field\n * @method OptionDefinition value(string $value) value for the form option\n * @method OptionDefinition readOnly(bool $readOnly) readOnly specifies if the option is read-only or not.\n * @method OptionDefinition disabled(bool $disabled) disabled specifies if the option is disabled or not.\n * @method OptionDefinition hidden(bool $hidden) hidden defines the option without ever displaying it\n * @method OptionDefinition color(string $color) color defines a status indicator color for the option as a hex color (dropdown)\n * @method OptionDefinition icon(string $icon) icon specifies an icon name for this option\n * @method OptionDefinition image(string $image) image specifies an image URL for this option\n * @method OptionDefinition children(array $image) children specifies child options for a nested structure\n *\n * @package october\\element\n * @author Alexey Bobkov, Samuel Georges\n */\nclass OptionDefinition extends ElementBase\n{\n    /**\n     * initDefaultValues for this field\n     */\n    protected function initDefaultValues()\n    {\n        $this\n            ->hidden(false)\n            ->readOnly(false)\n            ->disabled(false)\n            ->comment('');\n    }\n\n    /**\n     * useOptionConfig\n     */\n    public function useOptionConfig($value, $option): OptionDefinition\n    {\n        $this->value($value)->label($value);\n\n        // Option as string\n        if (!is_array($option)) {\n            $this->label($option);\n            return $this;\n        }\n\n        // Option as definition\n        if (Arr::isAssoc($option)) {\n            if (isset($option['children']) && is_array($option['children'])) {\n                $option['children'] = $this->evalChildOptions($option['children']);\n            }\n\n            $this->useConfig($option);\n            return $this;\n        }\n\n        // Option as [label, comment]\n        $firstPart = (string) ($option[0] ?? '');\n        $secondPart = (string) ($option[1] ?? '');\n\n        $this->label($firstPart);\n        $this->comment($secondPart);\n\n        if (Html::isValidColor($secondPart)) {\n            $this->color($secondPart);\n        }\n        elseif (strpos($secondPart, '.')) {\n            $this->image($secondPart);\n        }\n        else {\n            $this->icon($secondPart);\n        }\n\n        return $this;\n    }\n\n    /**\n     * evalChildOptions\n     */\n    protected function evalChildOptions(array $children): array\n    {\n        $result = [];\n\n        foreach ($children as $value => $option) {\n            $result[$value] = (new OptionDefinition)->useOptionConfig($value, $option);\n        }\n\n        return $result;\n    }\n}\n"
  },
  {
    "path": "src/Events/Dispatcher.php",
    "content": "<?php namespace October\\Rain\\Events;\n\nuse Illuminate\\Events\\Dispatcher as DispatcherBase;\n\n/**\n * Dispatcher proxy class\n *\n * @package october\\events\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Dispatcher extends DispatcherBase\n{\n    /**\n     * fire proxies to dispatch\n     */\n    public function fire(...$args)\n    {\n        return parent::dispatch(...$args);\n    }\n}\n"
  },
  {
    "path": "src/Events/EventServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Events;\n\nuse October\\Rain\\Events\\Dispatcher;\nuse Illuminate\\Support\\ServiceProvider;\nuse Illuminate\\Contracts\\Queue\\Factory as QueueFactoryContract;\n\n/**\n * EventServiceProvider\n *\n * @package october\\events\n * @author Alexey Bobkov, Samuel Georges\n */\nclass EventServiceProvider extends ServiceProvider\n{\n    /**\n     * register the service provider\n     */\n    public function register()\n    {\n        $this->app->singleton('events', function ($app) {\n            // return (new Dispatcher($app))->setQueueResolver(function () use ($app) {\n            //     return $app->make(QueueFactoryContract::class);\n            // })->setTransactionManagerResolver(function () use ($app) {\n            //     return $app->bound('db.transactions')\n            //         ? $app->make('db.transactions')\n            //         : null;\n            // });\n\n            // The following adds support for Laravel 10.30 when a transaction manager resolver\n            // was included as part of the dispatcher. Detect its presence and set it as needed\n            // @deprecated remove reflection and use code above in v4 (Laravel 11)\n            $dispatcher = (new Dispatcher($app))->setQueueResolver(function () use ($app) {\n                return $app->make(QueueFactoryContract::class);\n            });\n\n            if (method_exists($dispatcher, 'setTransactionManagerResolver')) {\n                $dispatcher->setTransactionManagerResolver(function () use ($app) {\n                    return $app->bound('db.transactions')\n                        ? $app->make('db.transactions')\n                        : null;\n                });\n            }\n\n            return $dispatcher;\n        });\n\n        $this->app->singleton('events.priority', function ($app) {\n            return (new PriorityDispatcher($app))->setLaravelDispatcher($app['events']);\n        });\n    }\n}\n"
  },
  {
    "path": "src/Events/FakeDispatcher.php",
    "content": "<?php namespace October\\Rain\\Events;\n\nuse Illuminate\\Support\\Testing\\Fakes\\EventFake as EventFakeBase;\n\n/**\n * FakeDispatcher\n *\n * @package october\\events\n * @author Alexey Bobkov, Samuel Georges\n */\nclass FakeDispatcher extends EventFakeBase\n{\n    /**\n     * __construct a new event fake instance.\n     */\n    public function __construct($dispatcher, $eventsToFake = [])\n    {\n        parent::__construct(\n            $dispatcher instanceof PriorityDispatcher ? $dispatcher->getLaravelDispatcher() : $dispatcher,\n            $eventsToFake\n        );\n    }\n\n    /**\n     * fire proxies to dispatch\n     */\n    public function fire(...$args)\n    {\n        return parent::dispatch(...$args);\n    }\n}\n"
  },
  {
    "path": "src/Events/PriorityDispatcher.php",
    "content": "<?php namespace October\\Rain\\Events;\n\nuse Str;\nuse Illuminate\\Container\\Container;\nuse Illuminate\\Contracts\\Events\\Dispatcher as DispatcherContract;\nuse Illuminate\\Contracts\\Container\\Container as ContainerContract;\n\n/**\n * PriorityDispatcher is a global event emitter with priority assignment.\n *\n * @package october\\events\n * @author Alexey Bobkov, Samuel Georges\n */\nclass PriorityDispatcher\n{\n    use \\October\\Rain\\Support\\Traits\\Emitter;\n    use \\Illuminate\\Support\\Traits\\ForwardsCalls;\n\n    const FORWARD_CALL_FLAG = '___FORWARD_CALL___';\n\n    /**\n     * @var \\Illuminate\\Contracts\\Container\\Container container for IoC\n     */\n    protected $container;\n\n    /**\n     * @var DispatcherContract laravelEvents instance.\n     */\n    protected $laravelEvents;\n\n    /**\n     * __construct a new event dispatcher instance.\n     * @param  \\Illuminate\\Contracts\\Container\\Container|null  $container\n     * @return void\n     */\n    public function __construct(?ContainerContract $container = null)\n    {\n        $this->container = $container ?: new Container;\n    }\n\n    /**\n     * listen registers an event listener with the dispatcher.\n     * @param string|array $events\n     * @param mixed|null $listener\n     * @param int $priority\n     * @return void\n     */\n    public function listen($events, $listener = null, $priority = 0)\n    {\n        if ($priority === 0) {\n            $this->laravelEvents->listen($events, $listener);\n        }\n        else {\n            $this->bindEvent($events, $listener, $priority);\n        }\n    }\n\n    /**\n     * subscribe registers an event subscriber with the dispatcher, passing\n     * the PriorityDispatcher instance so priority arguments are respected.\n     * @param object|string $subscriber\n     * @return void\n     */\n    public function subscribe($subscriber)\n    {\n        if (is_string($subscriber)) {\n            $subscriber = $this->container->make($subscriber);\n        }\n\n        $events = $subscriber->subscribe($this);\n\n        if (is_array($events)) {\n            foreach ($events as $event => $listeners) {\n                foreach ((array) $listeners as $listener) {\n                    if (is_string($listener) && method_exists($subscriber, $listener)) {\n                        $this->listen($event, [get_class($subscriber), $listener]);\n                        continue;\n                    }\n\n                    $this->listen($event, $listener);\n                }\n            }\n        }\n    }\n\n    /**\n     * listenOnce registers an event that only fires once.\n     * @param string|array $events\n     * @param callable $listener\n     * @param int $priority\n     * @return void\n     */\n    public function listenOnce($events, $listener, $priority = 0)\n    {\n        $this->bindEventOnce($events, $listener, $priority);\n    }\n\n    /**\n     * fire an event and call the listeners.\n     * @param string|object $event\n     * @param mixed $payload\n     * @param bool $halt\n     * @return array|null\n     */\n    public function fire($event, $payload = [], $halt = false)\n    {\n        return $this->fireEvent($event, $payload, $halt);\n    }\n\n    /**\n     * forget removes a set of listeners from the dispatcher.\n     * @param  string  $event\n     * @return void\n     */\n    public function forget($event)\n    {\n        $this->unbindEvent($event);\n\n        $this->laravelEvents->forget($event);\n    }\n\n    /**\n     * fireEvent inherits logic from the Emitter, modified to forward call to Laravel events\n     * @param string $event\n     * @param array $params\n     * @param boolean $halt\n     * @return array\n     */\n    public function fireEvent($event, $params = [], $halt = false)\n    {\n        if (!is_array($params)) {\n            $params = [$params];\n        }\n\n        // Micro optimization\n        if (\n            !isset($this->emitterEventCollection[$event]) &&\n            !isset($this->emitterSingleEventCollection[$event])\n        ) {\n            return $this->laravelEvents->dispatch($event, $params, $halt);\n        }\n\n        if (!isset($this->emitterEventSorted[$event])) {\n            $this->emitterEventSorted[$event] = $this->emitterEventSortEvents($event, [\n                0 => [self::FORWARD_CALL_FLAG]\n            ]);\n        }\n\n        $result = [];\n        foreach ($this->emitterEventSorted[$event] as $callback) {\n            if ($callback === self::FORWARD_CALL_FLAG) {\n                $response = $this->laravelEvents->dispatch($event, $params, $halt);\n                $isLaravel = true;\n            }\n            else {\n                if (is_string($callback)) {\n                    $callback = $this->createClassCallback($callback);\n                }\n\n                if (is_array($callback) && isset($callback[0]) && is_string($callback[0])) {\n                    $callback = $this->createClassCallback($callback);\n                }\n\n                $response = $callback(...$params);\n                $isLaravel = false;\n            }\n\n            if (!is_null($response) && $halt) {\n                return $response;\n            }\n\n            if ($response === false) {\n                break;\n            }\n\n            if (!is_null($response)) {\n                if ($isLaravel) {\n                    $result = array_merge($result, $response);\n                }\n                else {\n                    $result[] = $response;\n                }\n            }\n        }\n\n        if (isset($this->emitterSingleEventCollection[$event])) {\n            unset($this->emitterSingleEventCollection[$event]);\n            unset($this->emitterEventSorted[$event]);\n        }\n\n        return $halt ? null : $result;\n    }\n\n    /**\n     * setLaravelDispatcher sets the event resolver implementation.\n     */\n    public function setLaravelDispatcher(DispatcherContract $dispatcher): PriorityDispatcher\n    {\n        $this->laravelEvents = $dispatcher;\n\n        return $this;\n    }\n\n    /**\n     * getLaravelDispatcher returns the base event resolver.\n     */\n    public function getLaravelDispatcher(): DispatcherContract\n    {\n        return $this->laravelEvents;\n    }\n\n    /**\n     * createClassCallback passes what is usually a static method call through the IoC\n     * container to create a callable instance.\n     */\n    protected function createClassCallback($callback)\n    {\n        if (is_callable($callback)) {\n            return $callback;\n        }\n\n        [$class, $method] = is_array($callback)\n            ? $callback\n            : Str::parseCallback($callback, 'handle');\n\n        if (!method_exists($class, $method)) {\n            $method = '__invoke';\n        }\n\n        $listener = $this->container->make($class);\n\n        return [$listener, $method];\n    }\n\n    /**\n     * __call magic\n     * @param string $method\n     * @param array $parameters\n     * @return mixed\n     */\n    public function __call($method, $parameters)\n    {\n        return $this->forwardCallTo(\n            $this->laravelEvents,\n            $method,\n            $parameters\n        );\n    }\n}\n"
  },
  {
    "path": "src/Exception/AjaxException.php",
    "content": "<?php namespace October\\Rain\\Exception;\n\nuse Larajax\\Contracts\\AjaxExceptionInterface;\n\n/**\n * AjaxException is considered a \"smart error\" and will send http code 406,\n * so they can pass response contents.\n *\n * @package october\\exception\n * @author Alexey Bobkov, Samuel Georges\n */\nclass AjaxException extends ExceptionBase implements AjaxExceptionInterface\n{\n    /**\n     * @var array contents of the response.\n     */\n    protected $contents;\n\n    /**\n     * __construct the exception\n     */\n    public function __construct($contents = null)\n    {\n        if (is_string($contents)) {\n            $contents = ['result' => $contents];\n        }\n        elseif (!is_array($contents)) {\n            $contents = [];\n        }\n\n        $this->contents = $contents;\n\n        parent::__construct(json_encode($contents));\n    }\n\n    /**\n     * getContents returns invalid fields.\n     */\n    public function getContents()\n    {\n        return $this->contents;\n    }\n\n    /**\n     * addContent is used to add extra data to an AJAX exception\n     */\n    public function addContent(string $key, $val)\n    {\n        $this->contents[$key] = $val;\n    }\n\n    /**\n     * toAjaxData\n     */\n    public function toAjaxData(): array\n    {\n        return (array) $this->contents;\n    }\n}\n"
  },
  {
    "path": "src/Exception/ApplicationException.php",
    "content": "<?php namespace October\\Rain\\Exception;\n\n/**\n * ApplicationException represents an application exception and\n * these are not reported in the error log.\n *\n * @package october\\exception\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ApplicationException extends ExceptionBase\n{\n}\n"
  },
  {
    "path": "src/Exception/ErrorHandler.php",
    "content": "<?php namespace October\\Rain\\Exception;\n\nuse App;\nuse Request;\nuse Throwable;\n\n/**\n * ErrorHandler handles application exception events.\n *\n * @package october\\exception\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ErrorHandler\n{\n    /**\n     * @var \\System\\Classes\\ExceptionBase activeMask used to mask any exception fired.\n     */\n    protected static $activeMask;\n\n    /**\n     * @var array maskLayers is  a collection of masks, so multiples can be applied in order.\n     */\n    protected static $maskLayers = [];\n\n    /**\n     * @var array notFoundExceptions will redirect to the 404 page when captured.\n     */\n    protected $notFoundExceptions = [\n        \\October\\Rain\\Exception\\NotFoundException::class,\n        \\Illuminate\\Database\\Eloquent\\ModelNotFoundException::class,\n        \\Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException::class\n    ];\n\n    /**\n     * handleException handles all exceptions from the framework workflow. This method will mask\n     * any foreign exceptions with a \"scent\" of the native application's exception, so it can\n     * render correctly when displayed on the error page.\n     * @param Throwable $proposedException The exception candidate that has been thrown.\n     * @return mixed Error page contents\n     */\n    public function handleException(Throwable $proposedException)\n    {\n        // Disable the error handler for test and CLI environment\n        if (App::runningUnitTests() || App::runningInConsole()) {\n            return;\n        }\n\n        // Detect AJAX request and use error 500\n        if (Request::ajax()) {\n            return $proposedException instanceof AjaxException\n                 ? $proposedException->getContents()\n                 : static::getDetailedMessage($proposedException);\n        }\n\n        // Clear the output buffer\n        while (ob_get_level()) {\n            ob_end_clean();\n        }\n\n        // Friendly error pages are used\n        if (($customError = $this->handleCustomError($proposedException)) !== null) {\n            return $customError;\n        }\n\n        // If the exception is already our brand, use it\n        if ($proposedException instanceof ExceptionBase) {\n            $exception = $proposedException;\n        }\n        // If there is an active mask prepared, use that\n        elseif (static::$activeMask !== null) {\n            $exception = static::$activeMask;\n            $exception->setMask($proposedException);\n        }\n        // Otherwise we should mask it with our own default scent\n        else {\n            $exception = new ApplicationException($proposedException->getMessage(), 0);\n            $exception->setMask($proposedException);\n        }\n\n        return $this->handleDetailedError($exception);\n    }\n\n    /**\n     * isNotFoundException returns true if the exception is 404-flavored\n     */\n    protected function isNotFoundException($exception)\n    {\n        foreach ($this->notFoundExceptions as $type) {\n            if ($exception instanceof $type) {\n                return true;\n            }\n        }\n        return false;\n    }\n\n    /**\n     * applyMask prepares a mask exception to be used when any exception fires.\n     * @param Exception $exception The mask exception.\n     * @return void\n     */\n    public static function applyMask(Throwable $exception)\n    {\n        if (static::$activeMask !== null) {\n            array_push(static::$maskLayers, static::$activeMask);\n        }\n\n        static::$activeMask = $exception;\n    }\n\n    /**\n     * removeMask destroys the prepared mask by applyMask()\n     * @return void\n     */\n    public static function removeMask()\n    {\n        if (count(static::$maskLayers) > 0) {\n            static::$activeMask = array_pop(static::$maskLayers);\n        }\n        else {\n            static::$activeMask = null;\n        }\n    }\n\n    /**\n     * getDetailedMessage returns a more descriptive error message.\n     * @param Exception $exception\n     * @return string\n     */\n    public static function getDetailedMessage($exception)\n    {\n        return sprintf(\n            '\"%s\" on line %s of %s',\n            $exception->getMessage(),\n            $exception->getLine(),\n            $exception->getFile()\n        );\n    }\n\n    //\n    // Overrides\n    //\n\n    /**\n     * handleCustomError checks if using a custom error page, if so return the contents.\n     * Return NULL if a custom error is not set up.\n     * @return mixed\n     */\n    public function handleCustomError($exception)\n    {\n    }\n\n    /**\n     * handleDetailedError displays the detailed system exception page.\n     * @return View Object containing the error page.\n     */\n    public function handleDetailedError($exception)\n    {\n        return 'Error: ' . $exception->getMessage();\n    }\n}\n"
  },
  {
    "path": "src/Exception/ExceptionBase.php",
    "content": "<?php namespace October\\Rain\\Exception;\n\nuse File;\nuse Throwable;\nuse Exception;\n\n/**\n * ExceptionBase class represents a base interface and set of properties\n * for system and application exceptions.\n *\n * @package october\\exception\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ExceptionBase extends Exception\n{\n    /**\n     * @var Exception mask used when this exception is acting as a mask,\n     * this property stores the face exception.\n     */\n    protected $mask;\n\n    /**\n     * @var string hint message to help the user with troubleshooting the error (optional).\n     */\n    public $hint;\n\n    /**\n     * @var array fileContent relating to the exception, each value of the array is a file\n     * line number.\n     */\n    protected $fileContent = [];\n\n    /**\n     * @var string className of the called Exception.\n     */\n    protected $className;\n\n    /**\n     * @var string errorType derived from the error code, will be 'Undefined' if no code is used.\n     */\n    protected $errorType;\n\n    /**\n     * @var stdObject highlight cached code information for highlighting code.\n     */\n    protected $highlight;\n\n    /**\n     * __construct the CMS base exception class, which inherits the native PHP Exception.\n     * All CMS related classes should inherit this class, it creates a set of unified properties\n     * and an interface for displaying the CMS exception page.\n     * @param string $message Error message.\n     * @param int $code Error code.\n     * @param Throwable $previous Previous exception.\n     */\n    public function __construct($message = \"\", $code = 0, ?Throwable $previous = null)\n    {\n        if ($this->className === null) {\n            $this->className = get_called_class();\n        }\n\n        if ($this->errorType === null) {\n            $this->errorType = 'Undefined';\n        }\n\n        parent::__construct($message, $code, $previous);\n    }\n\n    /**\n     * getClassName returns the class name of the called Exception.\n     * @return string\n     */\n    public function getClassName()\n    {\n        return $this->className;\n    }\n\n    /**\n     * getErrorType returns the error type derived from the error code used.\n     * @return string\n     */\n    public function getErrorType()\n    {\n        return $this->errorType;\n    }\n\n    /**\n     * getNiceFile returns a file that is suitable for sharing.\n     * @return string\n     */\n    public function getNiceFile()\n    {\n        return File::nicePath($this->getFile());\n    }\n\n    /**\n     * mask an exception with the called class. This should catch fatal and php errors.\n     * It should always be followed by the unmask() method to remove the mask.\n     * @param string $message Error message.\n     * @param int $code Error code.\n     * @return void\n     */\n    public static function mask($message = null, $code = 0)\n    {\n        $calledClass = get_called_class();\n        $exception = new $calledClass($message, $code);\n        ErrorHandler::applyMask($exception);\n    }\n\n    /**\n     * unmask removes the active mask from the called class.\n     */\n    public static function unmask()\n    {\n        ErrorHandler::removeMask();\n    }\n\n    /**\n     * setMask is used if this exception acts as a mask, sets the face for the foreign exception.\n     * @param Throwable $exception Face for the mask, the underlying exception.\n     * @return void\n     */\n    public function setMask(Throwable $exception)\n    {\n        $this->mask = $exception;\n        $this->applyMask($exception);\n    }\n\n    /**\n     * applyMask is used if this method is used when applying the mask exception to the face\n     * exception. It can be used as an override for child classes who may use different\n     * masking logic.\n     * @param Throwable $exception Face exception being masked.\n     * @return void\n     */\n    public function applyMask(Throwable $exception)\n    {\n        $this->file = $exception->getFile();\n        $this->message = $exception->getMessage();\n        $this->line = $exception->getLine();\n        $this->className = get_class($exception);\n    }\n\n    /**\n     * getTrueException is used if this exception is acting as a mask, return the face exception.\n     * Otherwise return this exception as the true one.\n     * @return Throwable The underlying exception, or this exception if no mask is applied.\n     */\n    public function getTrueException()\n    {\n        if ($this->mask !== null) {\n            return $this->mask;\n        }\n\n        return $this;\n    }\n\n    /**\n     * getHighlight generates information used for highlighting the area of code in context of the\n     * exception line number. The highlighted block of code will be six (6) lines before and after\n     * the problem line number.\n     * @return object Highlight information as an array, the following keys are supplied:\n     * startLine - The starting line number, 6 lines before the error line.\n     * endLine - The ending line number, 6 lines after the error line.\n     * errorLine - The focused error line number.\n     * lines - An array of all the lines to be highlighted, each value is a line of code.\n     */\n    public function getHighlight()\n    {\n        if ($this->highlight !== null) {\n            return $this->highlight;\n        }\n\n        if (!$this->fileContent && File::exists($this->file) && is_readable($this->file)) {\n            $this->fileContent = @file($this->file) ?: [];\n        }\n\n        $errorLine = $this->line - 1;\n        $startLine = $errorLine - 6;\n\n        if ($startLine < 0) {\n            $startLine = 0;\n        }\n\n        $endLine = $startLine + 12;\n        $lineNum = count($this->fileContent);\n        if ($endLine > $lineNum-1) {\n            $endLine = $lineNum-1;\n        }\n\n        $areaLines = array_slice($this->fileContent, $startLine, $endLine - $startLine + 1);\n\n        $result = [\n            'startLine' => $startLine,\n            'endLine' => $endLine,\n            'errorLine' => $errorLine,\n            'lines' => []\n        ];\n\n        foreach ($areaLines as $index => $line) {\n            $result['lines'][$startLine + $index] = $line;\n        }\n\n        return $this->highlight = (object) $result;\n    }\n\n    /**\n     * getHighlightLines returns an array of line numbers used for highlighting the problem area\n     * of code. This will be six (6) lines before and after the error line number.\n     * @return array Array of code lines.\n     */\n    public function getHighlightLines()\n    {\n        $lines = $this->getHighlight()->lines;\n        foreach ($lines as $index => $line) {\n            $lines[$index] = strlen(trim($line)) ? htmlentities($line) : '&nbsp;'.PHP_EOL;\n        }\n        return $lines;\n    }\n\n    /**\n     * getCallStack returns the call stack as an array containing a stack information object.\n     * @return Array with stack information, each value will be an object with these values:\n     * id - The stack ID number.\n     * code - The class and function name being called.\n     * args - The arguments passed to the code function above.\n     * file - Reference to the file containing the called code.\n     * line - Reference to the line number of the file.\n     */\n    public function getCallStack()\n    {\n        $result = [];\n        $traceInfo = $this->filterCallStack($this->getTrueException()->getTrace());\n        $lastIndex = count($traceInfo) - 1;\n\n        foreach ($traceInfo as $index => $event) {\n            if (!isset($event['function'])) {\n                $event['function'] = null;\n            }\n\n            $functionName = (isset($event['class']) && strlen($event['class']))\n                ? $event['class'].$event['type'].$event['function']\n                : $event['function'];\n\n            $file = isset($event['file']) ? '~'.File::localToPublic($event['file']) : null;\n            $line = $event['line'] ?? null;\n\n            $args = null;\n            if (isset($event['args']) && count($event['args'])) {\n                $args = $this->formatStackArguments($event['args'], false);\n            }\n\n            $result[] = (object)[\n                'id'   => $lastIndex - $index + 1,\n                'code' => $functionName,\n                'args' => $args ? htmlentities($args) : '',\n                'file' => $file,\n                'line' => $line\n            ];\n        }\n\n        return $result;\n    }\n\n    /**\n     * filterCallStack removes the final steps of a call stack, which add no value for the user.\n     * The following exceptions and any trace information afterwards will be filtered:\n     * - Illuminate\\Foundation\\Bootstrap\\HandleExceptions\n     *\n     * @param array $traceInfo The trace information from getTrace() or debug_backtrace().\n     * @return array The filtered array containing the trace information.\n     */\n    protected function filterCallStack($traceInfo)\n    {\n        /*\n         * Determine if filter should be used at all.\n         */\n        $useFilter = false;\n        foreach ($traceInfo as $event) {\n            if (\n                isset($event['class']) &&\n                $event['class'] === 'Illuminate\\Foundation\\Bootstrap\\HandleExceptions' &&\n                $event['function'] === 'handleError'\n            ) {\n                $useFilter = true;\n            }\n        }\n\n        if (!$useFilter) {\n            return $traceInfo;\n        }\n\n        $filterResult = [];\n        $pruneResult = true;\n        foreach ($traceInfo as $index => $event) {\n            /*\n             * Prune the tail end of the trace from the framework exception handler.\n             */\n            if (\n                isset($event['class']) &&\n                $event['class'] === 'Illuminate\\Foundation\\Bootstrap\\HandleExceptions' &&\n                $event['function'] === 'handleError'\n            ) {\n                $pruneResult = false;\n                continue;\n            }\n\n            if ($pruneResult) {\n                continue;\n            }\n\n            $filterResult[$index] = $event;\n        }\n\n        return $filterResult;\n    }\n\n    /**\n     * formatStackArguments prepares a function or method argument list for display in HTML or text format\n     * @param array $arguments A list of the function or method arguments\n     * @return string\n     */\n    protected function formatStackArguments($arguments)\n    {\n        $argsArray = [];\n        foreach ($arguments as $argument) {\n            $arg = null;\n\n            if (is_array($argument)) {\n                $items = [];\n\n                foreach ($argument as $index => $obj) {\n                    if (is_array($obj)) {\n                        $value = 'array('.count($obj).')';\n                    }\n                    elseif (is_object($obj)) {\n                        $value = 'object('.get_class($obj).')';\n                    }\n                    elseif (is_int($obj)) {\n                        $value = $obj;\n                    }\n                    elseif ($obj === null) {\n                        $value = \"null\";\n                    }\n                    else {\n                        $value = \"'\".$obj.\"'\";\n                    }\n\n                    $items[] = $index . ' => ' . $value;\n                }\n\n                if (count($items)) {\n                    $arg = 'array(' . count($argument) . ') [' . implode(', ', $items) . ']';\n                }\n                else {\n                    $arg = 'array(0)';\n                }\n            }\n            elseif (is_object($argument)) {\n                $arg = 'object('.get_class($argument).')';\n            }\n            elseif ($argument === null) {\n                $arg = \"null\";\n            }\n            elseif (is_int($argument)) {\n                $arg = $argument;\n            }\n            else {\n                $arg = \"'\".$argument.\"'\";\n            }\n\n            $argsArray[] = $arg;\n        }\n\n        return implode(', ', $argsArray);\n    }\n}\n"
  },
  {
    "path": "src/Exception/ForbiddenException.php",
    "content": "<?php namespace October\\Rain\\Exception;\n\n/**\n * ForbiddenException represents a permission denied exception and\n * these will redirect to the nearest access denied / 403 page.\n *\n * @package october\\exception\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ForbiddenException extends ExceptionBase\n{\n}\n"
  },
  {
    "path": "src/Exception/NotFoundException.php",
    "content": "<?php namespace October\\Rain\\Exception;\n\n/**\n * NotFoundException represents a missing record exception and\n * these will redirect to the nearest 404 page.\n *\n * @package october\\exception\n * @author Alexey Bobkov, Samuel Georges\n */\nclass NotFoundException extends ExceptionBase\n{\n}\n"
  },
  {
    "path": "src/Exception/SystemException.php",
    "content": "<?php namespace October\\Rain\\Exception;\n\n/**\n * SystemException represents a critical system exception and\n * these are reported in the error log.\n *\n * @package october\\exception\n * @author Alexey Bobkov, Samuel Georges\n */\nclass SystemException extends ExceptionBase\n{\n}\n"
  },
  {
    "path": "src/Exception/ValidationException.php",
    "content": "<?php namespace October\\Rain\\Exception;\n\nuse Validator as ValidatorFacade;\nuse Illuminate\\Validation\\ValidationException as ValidationExceptionBase;\nuse Illuminate\\Validation\\Validator;\nuse Illuminate\\Support\\MessageBag;\nuse InvalidArgumentException;\n\n/**\n * ValidationException class\n *\n * @package october\\exception\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ValidationException extends ValidationExceptionBase\n{\n    /**\n     * @var array fields that are invalid\n     */\n    protected $fields;\n\n    /**\n     * @var array fieldPrefix\n     */\n    protected $fieldPrefix = [];\n\n    /**\n     * @var \\Illuminate\\Support\\MessageBag errors in the form of a message bag\n     */\n    protected $errors;\n\n    /**\n     * __construct the validation exception.\n     */\n    public function __construct($validation)\n    {\n        parent::__construct($this->resolveToValidator($validation));\n\n        $this->evalErrors();\n    }\n\n    /**\n     * resolveToValidator resolves general input for the validation exception\n     * @param  mixed  $validation\n     */\n    protected function resolveToValidator($validation)\n    {\n        $validator = $validation;\n\n        if (is_null($validation)) {\n            $validator = ValidatorFacade::make([], []);\n        }\n        elseif (is_array($validation)) {\n            $validator = ValidatorFacade::make([], []);\n            $validator->errors()->merge($validation);\n        }\n        elseif ($validation instanceof MessageBag) {\n            $validator = ValidatorFacade::make([], []);\n            $validator->errors()->merge($validation->messages());\n        }\n\n        if (!$validator instanceof Validator) {\n            throw new InvalidArgumentException('ValidationException constructor requires instance of Validator or array');\n        }\n\n        return $validator;\n    }\n\n    /**\n     * evalErrors evaluates errors\n     */\n    protected function evalErrors()\n    {\n        $this->fields = [];\n\n        foreach ($this->errors() as $field => $messages) {\n            $fieldName = implode('.', array_merge($this->fieldPrefix, [$field]));\n            $this->fields[$fieldName] = (array) $messages;\n        }\n\n        $this->message = $this->getErrors()->first();\n    }\n\n    /**\n     * getErrors returns directly the message bag instance with the model's errors\n     * @return \\Illuminate\\Support\\MessageBag\n     */\n    public function getErrors()\n    {\n        return $this->validator->errors();\n    }\n\n    /**\n     * @deprecated use ->errors()\n     */\n    public function getFields()\n    {\n        return $this->fields;\n    }\n\n    /**\n     * setFieldPrefix increases the field target specificity\n     */\n    public function setFieldPrefix(array $prefix)\n    {\n        $this->fieldPrefix = array_filter($prefix, 'strlen');\n\n        $this->evalErrors();\n\n        $this->validator = $this->resolveToValidator($this->fields);\n    }\n}\n"
  },
  {
    "path": "src/Extension/Container.php",
    "content": "<?php namespace October\\Rain\\Extension;\n\n/**\n * Container holds constructor logic for all extensions\n *\n * @package october\\extension\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Container\n{\n    /**\n     * @var array classCallbacks is used to extend the constructor of an extendable class. Eg:\n     *\n     *     Class::extend(function($obj) { })\n     *\n     */\n    public static $classCallbacks = [];\n\n    /**\n     * @var array Used to extend the constructor of an extension class. Eg:\n     *\n     *     BehaviorClass::extend(function($obj) { })\n     *\n     */\n    public static $extensionCallbacks = [];\n\n    /**\n     * extendClass extends a class without including it\n     */\n    public static function extendClass(string $class, callable $callback)\n    {\n        self::$classCallbacks[$class][] = $callback;\n    }\n\n    /**\n     * extendBehavior extends a class without including it\n     */\n    public static function extendBehavior(string $class, callable $callback)\n    {\n        self::$extensionCallbacks[$class][] = $callback;\n    }\n\n    /**\n     * clearExtensions clears the list of extended classes so they will be re-extended\n     */\n    public static function clearExtensions()\n    {\n        self::$classCallbacks = [];\n        self::$extensionCallbacks = [];\n    }\n}\n"
  },
  {
    "path": "src/Extension/Extendable.php",
    "content": "<?php namespace October\\Rain\\Extension;\n\n/**\n * Extendable class\n *\n * If a class extends this class, it will enable support for using \"Private traits\".\n *\n * Usage:\n *\n *     public $implement = [\\Path\\To\\Some\\Namespace\\Class::class];\n *\n * See the `ExtensionBase` class for creating extension classes.\n *\n * @package october\\extension\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Extendable\n{\n    use ExtendableTrait;\n\n    /**\n     * @var array implement extensions for this class.\n     */\n    public $implement = [];\n\n    /**\n     * __construct the extendable class\n     */\n    public function __construct()\n    {\n        $this->extendableConstruct();\n    }\n\n    /**\n     * __get an undefined property\n     */\n    public function __get($name)\n    {\n        return $this->extendableGet($name);\n    }\n\n    /**\n     * __set an undefined property\n     */\n    public function __set($name, $value)\n    {\n        $this->extendableSet($name, $value);\n    }\n\n    /**\n     * __call calls an undefined local method\n     */\n    public function __call($name, $params)\n    {\n        return $this->extendableCall($name, $params);\n    }\n\n    /**\n     * __callStatic calls an undefined static method\n     */\n    public static function __callStatic($name, $params)\n    {\n        return self::extendableCallStatic($name, $params);\n    }\n\n    /**\n     * __sleep prepare the object for serialization.\n     */\n    public function __sleep()\n    {\n        $this->extendableDestruct();\n\n        return array_keys(get_object_vars($this));\n    }\n\n    /**\n     * __wakeup when a model is being unserialized, check if it needs to be booted.\n     */\n    public function __wakeup()\n    {\n        $this->extendableConstruct();\n    }\n\n    /**\n     * extend this class with a closure\n     */\n    public static function extend(callable $callback)\n    {\n        self::extendableExtendCallback($callback);\n    }\n}\n"
  },
  {
    "path": "src/Extension/ExtendableTrait.php",
    "content": "<?php namespace October\\Rain\\Extension;\n\nuse ReflectionClass;\nuse ReflectionMethod;\nuse ReflectionFunction;\nuse ReflectionFunctionAbstract;\nuse BadMethodCallException;\nuse Exception;\n\n/**\n * ExtendableTrait trait is used when access to the underlying base class\n * is not available, such as classes that belong to the foundation\n * framework (Laravel). It is currently used by the Controller and\n * Model classes.\n *\n * @package october\\extension\n * @see \\October\\Rain\\Extension\\Extendable\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait ExtendableTrait\n{\n    /**\n     * @var array extensionData contains class reflection information, including behaviors\n     */\n    protected $extensionData = [\n        'extensions' => [],\n        'methods' => [],\n        'dynamicMethods' => [],\n        'dynamicProperties' => []\n    ];\n\n    /**\n     * @var array extendableStaticMethods is a collection of static methods used by behaviors\n     */\n    protected static $extendableStaticMethods = [];\n\n    /**\n     * extendableConstruct should be called as part of the constructor\n     */\n    public function extendableConstruct()\n    {\n        // Apply init callbacks\n        $classes = array_merge([static::class], class_parents(static::class));\n        foreach ($classes as $class) {\n            if (isset(Container::$classCallbacks[$class]) && is_array(Container::$classCallbacks[$class])) {\n                foreach (Container::$classCallbacks[$class] as $callback) {\n                    call_user_func($callback, $this);\n                }\n            }\n        }\n\n        // Apply extensions\n        foreach ($this->extensionExtractImplements() as $useClass) {\n            // Previously soft implemented behaviors started with @ (backward compatibility)\n            if (substr($useClass, 0, 1) === '@') {\n                $useClass = substr($useClass, 1);\n            }\n\n            if (!class_exists($useClass)) {\n                continue;\n            }\n\n            $this->extendClassWith($useClass);\n        }\n    }\n\n    /**\n     * extendableDestruct should be called when serializing the object\n     */\n    public function extendableDestruct()\n    {\n        $this->extensionData = [\n            'extensions' => [],\n            'methods' => [],\n            'dynamicMethods' => [],\n            'dynamicProperties' => []\n        ];\n    }\n\n    /**\n     * extendableExtendCallback is a helper method for `::extend()` static method\n     * @param  callable $callback\n     * @return void\n     */\n    public static function extendableExtendCallback($callback)\n    {\n        $class = get_called_class();\n        if (\n            !isset(Container::$classCallbacks[$class]) ||\n            !is_array(Container::$classCallbacks[$class])\n        ) {\n            Container::$classCallbacks[$class] = [];\n        }\n\n        Container::$classCallbacks[$class][] = $callback;\n    }\n\n    /**\n     * extensionExtractImplements will return classes to implement.\n     */\n    protected function extensionExtractImplements(): array\n    {\n        if (!$this->implement) {\n            return [];\n        }\n\n        if (is_string($this->implement)) {\n            $uses = explode(',', $this->implement);\n        }\n        elseif (is_array($this->implement)) {\n            $uses = $this->implement;\n        }\n        else {\n            throw new Exception(sprintf('Class %s contains an invalid $implement value', static::class));\n        }\n\n        foreach ($uses as &$use) {\n            $use = str_replace('.', '\\\\', trim($use));\n        }\n\n        return $uses;\n    }\n\n    /**\n     * extensionExtractMethods extracts the available methods from a behavior and adds it\n     * to the list of callable methods\n     * @param  string $extensionName\n     * @param  object $extensionObject\n     * @return void\n     */\n    protected function extensionExtractMethods($extensionName, $extensionObject)\n    {\n        if (!method_exists($extensionObject, 'extensionIsHiddenMethod')) {\n            throw new Exception(sprintf(\n                'Extension %s should inherit October\\Rain\\Extension\\ExtensionBase or implement October\\Rain\\Extension\\ExtensionTrait.',\n                $extensionName\n            ));\n        }\n\n        $extensionMethods = get_class_methods($extensionName);\n        foreach ($extensionMethods as $methodName) {\n            if (\n                $methodName === '__construct' ||\n                $extensionObject->extensionIsHiddenMethod($methodName)\n            ) {\n                continue;\n            }\n\n            $this->extensionData['methods'][$methodName] = $extensionName;\n        }\n    }\n\n    /**\n     * addDynamicMethod programmatically adds a method to the extendable class\n     * @param string   $dynamicName\n     * @param callable $method\n     * @param string   $extension\n     */\n    public function addDynamicMethod($dynamicName, $method, $extension = null)\n    {\n        if (\n            is_string($method) &&\n            $extension &&\n            ($extensionObj = $this->getClassExtension($extension))\n        ) {\n            $method = [$extensionObj, $method];\n        }\n\n        $this->extensionData['dynamicMethods'][$dynamicName] = $method;\n    }\n\n    /**\n     * addDynamicProperty programmatically adds a property to the extendable class\n     * @param string $dynamicName\n     * @param string $value\n     */\n    public function addDynamicProperty($dynamicName, $value = null)\n    {\n        if (\n            property_exists($this, $dynamicName) ||\n            array_key_exists($dynamicName, $this->extensionData['dynamicProperties'])\n        ) {\n            return;\n        }\n\n        $this->extensionData['dynamicProperties'][$dynamicName] = $value;\n    }\n\n    /**\n     * extendClassWith dynamically extends a class with a specified behavior\n     * @param  string $extensionName\n     * @return void\n     */\n    public function extendClassWith($extensionName)\n    {\n        if (!strlen($extensionName)) {\n            return;\n        }\n\n        $extensionName = str_replace('.', '\\\\', trim($extensionName));\n\n        if (isset($this->extensionData['extensions'][$extensionName])) {\n            throw new Exception(sprintf(\n                'Class %s has already been extended with %s',\n                static::class,\n                $extensionName\n            ));\n        }\n\n        $this->extensionData['extensions'][$extensionName] = $extensionObject = new $extensionName($this);\n        $this->extensionExtractMethods($extensionName, $extensionObject);\n        $extensionObject->extensionApplyInitCallbacks();\n    }\n\n    /**\n     * isClassExtendedWith checks if extendable class is extended with a behavior object\n     * @param  string $name Fully qualified behavior name\n     * @return boolean\n     */\n    public function isClassExtendedWith($name)\n    {\n        $name = str_replace('.', '\\\\', trim($name));\n        return isset($this->extensionData['extensions'][$name]);\n    }\n\n    /**\n     * implementClassWith will implement an extension using non-interference and should\n     * be used with the static extend() method.\n     */\n    public function implementClassWith($extensionName)\n    {\n        $extensionName = str_replace('.', '\\\\', trim($extensionName));\n\n        if (in_array($extensionName, $this->extensionExtractImplements())) {\n            return;\n        }\n\n        $this->implement[] = $extensionName;\n    }\n\n    /**\n     * isClassInstanceOf checks if the class implements the supplied interface methods.\n     */\n    public function isClassInstanceOf($interface): bool\n    {\n        $classMethods = $this->getClassMethods();\n\n        if (is_string($interface) && !interface_exists($interface)) {\n            throw new Exception(sprintf(\n                'Interface %s does not exist',\n                $interface\n            ));\n        }\n\n        $interfaceMethods = (array) get_class_methods($interface);\n        foreach ($interfaceMethods as $methodName) {\n            if (!in_array($methodName, $classMethods)) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * getClassExtension returns a behavior object from an extendable class, example:\n     *\n     *     $this->getClassExtension('Backend.Behaviors.FormController')\n     *\n     * @param  string $name Fully qualified behavior name\n     * @return mixed\n     */\n    public function getClassExtension($name)\n    {\n        $name = str_replace('.', '\\\\', trim($name));\n        return $this->extensionData['extensions'][$name] ?? null;\n    }\n\n    /**\n     * asExtension is short hand for `getClassExtension()` method, except takes the short\n     * extension name, example:\n     *\n     *     $this->asExtension('FormController')\n     *\n     * @param  string $shortName\n     * @return mixed\n     */\n    public function asExtension($shortName)\n    {\n        foreach ($this->extensionData['extensions'] as $class => $obj) {\n            if (\n                preg_match('@\\\\\\\\([\\w]+)$@', $class, $matches) &&\n                $matches[1] === $shortName\n            ) {\n                return $obj;\n            }\n        }\n\n        return $this->getClassExtension($shortName);\n    }\n\n    /**\n     * methodExists checks if a method exists, extension equivalent of method_exists()\n     * @param  string $name\n     * @return boolean\n     */\n    public function methodExists($name)\n    {\n        return (\n            method_exists($this, $name) ||\n            isset($this->extensionData['methods'][$name]) ||\n            isset($this->extensionData['dynamicMethods'][$name])\n        );\n    }\n\n    /**\n     * getClassMethods gets a list of class methods, extension equivalent of get_class_methods()\n     * @return array\n     */\n    public function getClassMethods()\n    {\n        return array_values(array_unique(array_merge(\n            get_class_methods($this),\n            array_keys($this->extensionData['methods']),\n            array_keys($this->extensionData['dynamicMethods'])\n        )));\n    }\n\n    /**\n     * getClassMethodAsReflector\n     */\n    public function getClassMethodAsReflector(string $name): ReflectionFunctionAbstract\n    {\n        $extendableMethod = $this->getExtendableMethodFromExtensions($name);\n        if ($extendableMethod !== null) {\n            return new ReflectionMethod($extendableMethod[0], $extendableMethod[1]);\n        }\n\n        $extendableDynamicMethod = $this->getExtendableMethodFromDynamicMethods($name);\n        if ($extendableDynamicMethod !== null) {\n            return new ReflectionFunction($extendableDynamicMethod);\n        }\n\n        return new ReflectionMethod($this, $name);\n    }\n\n    /**\n     * getDynamicProperties returns all dynamic properties and their values\n     * @return array ['property' => 'value']\n     */\n    public function getDynamicProperties()\n    {\n        return $this->extensionData['dynamicProperties'];\n    }\n\n    /**\n     * propertyExists checks if a property exists, extension equivalent of `property_exists()`\n     * @param  string $name\n     * @return boolean\n     */\n    public function propertyExists($name)\n    {\n        if (property_exists($this, $name)) {\n            return true;\n        }\n\n        foreach ($this->extensionData['extensions'] as $extensionObject) {\n            if (\n                property_exists($extensionObject, $name) &&\n                $this->extendableIsAccessible($extensionObject, $name)\n            ) {\n                return true;\n            }\n        }\n\n        if (array_key_exists($name, $this->extensionData['dynamicProperties'])) {\n            return true;\n        }\n\n        return false;\n    }\n\n    /**\n     * extendableIsAccessible checks if a property is accessible, property equivalent\n     * of `is_callable()`\n     * @param  mixed  $class\n     * @param  string $propertyName\n     * @return boolean\n     */\n    protected function extendableIsAccessible($class, $propertyName)\n    {\n        $reflector = new ReflectionClass($class);\n        $property = $reflector->getProperty($propertyName);\n        return $property->isPublic();\n    }\n\n    /**\n     * extendableGet magic method for `__get()`\n     * @param  string $name\n     * @return string\n     */\n    public function extendableGet($name)\n    {\n        foreach ($this->extensionData['extensions'] as $extensionObject) {\n            if (\n                property_exists($extensionObject, $name) &&\n                $this->extendableIsAccessible($extensionObject, $name)\n            ) {\n                return $extensionObject->{$name};\n            }\n        }\n\n        // Getting a dynamic property\n        if (array_key_exists($name, $this->extensionData['dynamicProperties'])) {\n            return $this->extensionData['dynamicProperties'][$name];\n        }\n\n        $parent = get_parent_class(self::class);\n        if ($parent !== false && method_exists($parent, '__get')) {\n            return parent::__get($name);\n        }\n    }\n\n    /**\n     * extendableSet magic method for `__set()`\n     * @param  string $name\n     * @param  string $value\n     * @return string\n     */\n    public function extendableSet($name, $value)\n    {\n        $found = false;\n\n        // Spin over each extension to find it\n        foreach ($this->extensionData['extensions'] as $extensionObject) {\n            if (!property_exists($extensionObject, $name)) {\n                continue;\n            }\n\n            $extensionObject->{$name} = $value;\n            $found = true;\n        }\n\n        // Setting a dynamic property\n        if (array_key_exists($name, $this->extensionData['dynamicProperties'])) {\n            $this->extensionData['dynamicProperties'][$name] = $value;\n            return;\n        }\n\n        // This targets trait usage in particular\n        $parent = get_parent_class(self::class);\n        if ($parent !== false && method_exists($parent, '__set')) {\n            parent::__set($name, $value);\n            $found = true;\n        }\n\n        // Undefined property, throw an exception to catch it\n        if (!$found) {\n            throw new BadMethodCallException(sprintf(\n                'Call to undefined property %s::%s',\n                static::class,\n                $name\n            ));\n        }\n    }\n\n    /**\n     * extendableCall magic method for `__call()`\n     * @param  string $name\n     * @param  array  $params\n     * @return mixed\n     */\n    public function extendableCall($name, $params = null)\n    {\n        $callable = $this->getExtendableMethodFromExtensions($name);\n\n        if ($callable === null) {\n            $callable = $this->getExtendableMethodFromDynamicMethods($name);\n        }\n\n        if ($callable !== null) {\n            return call_user_func_array($callable, $params);\n        }\n\n        $parent = get_parent_class(self::class);\n        if ($parent !== false && method_exists($parent, '__call')) {\n            return parent::__call($name, $params);\n        }\n\n        throw new BadMethodCallException(sprintf(\n            'Call to undefined method %s::%s()',\n            static::class,\n            $name\n        ));\n    }\n\n    /**\n     * extendableCallStatic magic method for `__callStatic()`\n     * @param  string $name\n     * @param  array  $params\n     * @return mixed\n     */\n    public static function extendableCallStatic($name, $params = null)\n    {\n        $className = get_called_class();\n\n        if (!array_key_exists($className, self::$extendableStaticMethods)) {\n            self::$extendableStaticMethods[$className] = [];\n\n            $class = new ReflectionClass($className);\n            $defaultProperties = $class->getDefaultProperties();\n            if (\n                array_key_exists('implement', $defaultProperties) &&\n                ($implement = $defaultProperties['implement'])\n            ) {\n                // Apply extensions\n                if (is_string($implement)) {\n                    $uses = explode(',', $implement);\n                }\n                elseif (is_array($implement)) {\n                    $uses = $implement;\n                }\n                else {\n                    throw new Exception(sprintf('Class %s contains an invalid $implement value', $className));\n                }\n\n                foreach ($uses as $use) {\n                    $useClassName = str_replace('.', '\\\\', trim($use));\n\n                    $useClass = new ReflectionClass($useClassName);\n                    $staticMethods = $useClass->getMethods(ReflectionMethod::IS_STATIC);\n                    foreach ($staticMethods as $method) {\n                        self::$extendableStaticMethods[$className][$method->getName()] = $useClassName;\n                    }\n                }\n            }\n        }\n\n        if (isset(self::$extendableStaticMethods[$className][$name])) {\n            $extension = self::$extendableStaticMethods[$className][$name];\n\n            if (method_exists($extension, $name) && is_callable([$extension, $name])) {\n                $extension::$extendableStaticCalledClass = $className;\n                $result = forward_static_call_array(array($extension, $name), $params);\n                $extension::$extendableStaticCalledClass = null;\n                return $result;\n            }\n        }\n\n        // $parent = get_parent_class($className);\n        // if ($parent !== false && method_exists($parent, '__callStatic')) {\n        //    return parent::__callStatic($name, $params);\n        // }\n\n        throw new BadMethodCallException(sprintf(\n            'Call to undefined method %s::%s()',\n            $className,\n            $name\n        ));\n    }\n\n    /**\n     * getExtendableMethodFromExtensions\n     */\n    protected function getExtendableMethodFromExtensions(string $name): ?array\n    {\n        if (!isset($this->extensionData['methods'][$name])) {\n            return null;\n        }\n\n        $extension = $this->extensionData['methods'][$name];\n        $extensionObject = $this->extensionData['extensions'][$extension];\n\n        if (!method_exists($extension, $name) || !is_callable([$extensionObject, $name])) {\n            return null;\n        }\n\n        return [$extensionObject, $name];\n    }\n\n    /**\n     * getExtendableMethodFromDynamicMethods\n     */\n    protected function getExtendableMethodFromDynamicMethods(string $name): ?callable\n    {\n        if (!isset($this->extensionData['dynamicMethods'][$name])) {\n            return null;\n        }\n\n        $dynamicCallable = $this->extensionData['dynamicMethods'][$name];\n\n        if (!is_callable($dynamicCallable)) {\n            return null;\n        }\n\n        return $dynamicCallable;\n    }\n\n    /**\n     * @deprecated use \\October\\Rain\\Extension\\Container::clearExtensions()\n     */\n    public static function clearExtendedClasses()\n    {\n        Container::clearExtensions();\n    }\n}\n"
  },
  {
    "path": "src/Extension/ExtensionBase.php",
    "content": "<?php namespace October\\Rain\\Extension;\n\n/**\n * ExtensionBase allows for \"private traits\"\n *\n * @package october\\extension\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ExtensionBase\n{\n    use ExtensionTrait;\n\n    /**\n     * extend this class with a closure\n     */\n    public static function extend(callable $callback)\n    {\n        self::extensionExtendCallback($callback);\n    }\n}\n"
  },
  {
    "path": "src/Extension/ExtensionTrait.php",
    "content": "<?php namespace October\\Rain\\Extension;\n\n/**\n * ExtensionTrait allows for \"private traits\"\n *\n * @package october\\extension\n * @see October\\Rain\\Extension\\ExtensionBase\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait ExtensionTrait\n{\n    /**\n     * @var string extendableStaticCalledClass is the calling class when using a static method.\n     */\n    public static $extendableStaticCalledClass = null;\n\n    /**\n     * @var array extensionHidden are properties and methods that cannot be accessed.\n     */\n    protected $extensionHidden = [\n        'methods' => ['extensionIsHiddenProperty', 'extensionIsHiddenMethod'],\n        'properties' => []\n    ];\n\n    /**\n     * extensionApplyInitCallbacks\n     */\n    public function extensionApplyInitCallbacks()\n    {\n        $classes = array_merge([static::class], class_parents($this));\n        foreach ($classes as $class) {\n            if (isset(Container::$extensionCallbacks[$class]) && is_array(Container::$extensionCallbacks[$class])) {\n                foreach (Container::$extensionCallbacks[$class] as $callback) {\n                    call_user_func($callback, $this);\n                }\n            }\n        }\n    }\n\n    /**\n     * extensionExtendCallback is a helper method for `::extend()` static method\n     * @param  callable $callback\n     * @return void\n     */\n    public static function extensionExtendCallback($callback)\n    {\n        $class = get_called_class();\n        if (\n            !isset(Container::$extensionCallbacks[$class]) ||\n            !is_array(Container::$extensionCallbacks[$class])\n        ) {\n            Container::$extensionCallbacks[$class] = [];\n        }\n\n        Container::$extensionCallbacks[$class][] = $callback;\n    }\n\n    /**\n     * extensionHideMethod\n     */\n    protected function extensionHideMethod($name)\n    {\n        $this->extensionHidden['methods'][] = $name;\n    }\n\n    /**\n     * extensionHideProperty\n     */\n    protected function extensionHideProperty($name)\n    {\n        $this->extensionHidden['properties'][] = $name;\n    }\n\n    /**\n     * extensionIsHiddenMethod\n     */\n    public function extensionIsHiddenMethod($name)\n    {\n        return in_array($name, $this->extensionHidden['methods']);\n    }\n\n    /**\n     * extensionIsHiddenProperty\n     */\n    public function extensionIsHiddenProperty($name)\n    {\n        return in_array($name, $this->extensionHidden['properties']);\n    }\n\n    /**\n     * getCalledExtensionClass\n     */\n    public static function getCalledExtensionClass()\n    {\n        return self::$extendableStaticCalledClass;\n    }\n}\n"
  },
  {
    "path": "src/Extension/README.md",
    "content": "## Rain Extensions\n\nAdds the ability for classes to have *private traits*, also known as Behaviors. These are similar to native PHP Traits except they have some distinct benefits:\n\n1. Behaviors have their own constructor.\n1. Behaviors can have private or protected methods.\n1. Methods and property names can conflict safely.\n1. Class can be extended with behaviors dynamically.\n\nWhere you might use a trait like this:\n\n    class MyClass\n    {\n        use \\October\\Rain\\UtilityFunctions;\n        use \\October\\Rain\\DeferredBinding;\n    }\n\nA behavior is used in a similar fashion:\n\n    class MyClass extends \\October\\Rain\\Extension\\Extendable\n    {\n        public $implement = [\n            'October.Rain.UtilityFunctions',\n            'October.Rain.DeferredBinding',\n        ];\n    }\n\nWhere you might define a trait like this:\n\n    trait UtilityFunctions\n    {\n        public function sayHello()\n        {\n            echo \"Hello from \" . get_class($this);\n        }\n    }\n\nA behavior is defined like this:\n\n    class UtilityFunctions extends \\October\\Rain\\Extension\\ExtensionBase\n    {\n        protected $parent;\n\n        public function __construct($parent)\n        {\n            $this->parent = $parent;\n        }\n\n        public function sayHello()\n        {\n            echo \"Hello from \" . get_class($this->parent);\n        }\n    }\n\nThe extended object is always passed as the first parameter to the Behavior's constructor.\n\n### Usage example\n\n#### Behavior / Extension class\n\n    <?php namespace MyNamespace\\Behaviors;\n\n    class FormController extends \\October\\Rain\\Extension\\ExtensionBase\n    {\n        /**\n         * @var Reference to the extended object.\n         */\n        protected $controller;\n\n        /**\n         * Constructor\n         */\n        public function __construct($controller)\n        {\n            $this->controller = $controller;\n        }\n\n        public function someMethod()\n        {\n            return \"I come from the FormController Behavior!\";\n        }\n\n        public function otherMethod()\n        {\n            return \"You might not see me...\";\n        }\n    }\n\n#### Extending a class\n\nThis `Controller` class will implement the `FormController` behavior and then the methods will become available (mixed in) to the class. We will override the `otherMethod` method.\n\n    <?php namespace MyNamespace;\n\n    class Controller extends \\October\\Rain\\Extension\\Extendable\n    {\n\n        /**\n         * Implement the FormController behavior\n         */\n        public $implement = [\n            'MyNamespace.Behaviors.FormController'\n        ];\n\n        public function otherMethod()\n        {\n            return \"I come from the main Controller!\";\n        }\n    }\n\n#### Using the extension\n\n    $controller = new MyNamespace\\Controller;\n\n    // Prints: I come from the FormController Behavior!\n    echo $controller->someMethod();\n\n    // Prints: I come from the main Controller!\n    echo $controller->otherMethod();\n\n    // Prints: You might not see me...\n    echo $controller->asExtension('FormController')->otherMethod();\n\n### Dynamically using a behavior / Constructor extension\n\nAny class that uses the `Extendable` or `ExtendableTrait` can have its constructor extended with the static `extend()` method. The argument should pass a closure that will be called as part of the class constructor. For example:\n\n    /**\n     * Extend the Pizza Shop to include the Master Splinter behavior too\n     */\n    MyNamespace\\Controller::extend(function($controller){\n\n        // Implement the list controller behavior dynamically\n        $controller->implement[] = 'MyNamespace.Behaviors.ListController';\n    });\n\n### Dynamically creating methods\nMethods can be added to a `Model` through the use of `addDynamicMethod`.\n\n    Post::extend(function($model) {\n        $model->addDynamicMethod('getTagsAttribute', function() use ($model) {\n            return $model->tags()->lists('name');\n        });\n    });\n\n### Soft definition\n\nIf a behavior class does not exist, like a trait, an *Class not found* error will be thrown. In some cases you may wish to suppress this error, for conditional implementation if a module is present in the system. You can do this by placing an `@` symbol at the beginning of the class name.\n\n    class User extends \\October\\Rain\\Extension\\Extendable\n    {\n        public $implement = ['@RainLab.Translate.Behaviors.TranslatableModel'];\n    }\n\nIf the class name `RainLab\\Translate\\Behaviors\\TranslatableModel` does not exist, no error will be thrown. This is the equivalent of the following code:\n\n    class User extends \\October\\Rain\\Extension\\Extendable\n    {\n        public $implement = [];\n\n        public function __construct()\n        {\n            if (class_exists('RainLab\\Translate\\Behaviors\\TranslatableModel')) {\n                $controller->implement[] = 'RainLab.Translate.Behaviors.TranslatableModel';\n            }\n\n            parent::__construct();\n        }\n    }\n\n### Using Traits instead of base classes\n\nIn some cases you may not wish to extend the `ExtensionBase` or `Extendable` classes, due to other needs. So you can use the traits instead, although obviously the behavior methods will not be available to the parent class.\n\n- When using the `ExtensionTrait` the methods from `ExtensionBase` should be applied to the class.\n\n- When using the `ExtendableTrait` the methods from `Extendable` should be applied to the class.\n"
  },
  {
    "path": "src/Filesystem/Definitions.php",
    "content": "<?php namespace October\\Rain\\Filesystem;\n\nuse Config;\nuse Exception;\n\n/**\n * Definitions contains file extensions for common use cases\n *\n * @package october\\filesystem\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Definitions\n{\n    /**\n     * getDefinitions is an entry point to request a definition set\n     */\n    public static function get(string $type): array\n    {\n        return (new self)->getDefinitions($type);\n    }\n\n    /**\n     * getDefinitions returns a definition set from config or from the default sets.\n     */\n    public function getDefinitions(string $type): array\n    {\n        $typeConfig = snake_case($type);\n        $typeMethod = studly_case($type);\n\n        if (!method_exists($this, $typeMethod)) {\n            throw new Exception(sprintf('No such definition set exists for \"%s\"', $type));\n        }\n\n        // Support dual configuration\n        return (array) Config::get('media.'.$typeConfig,\n            // @deprecated\n            Config::get('cms.file_definitions.'.$typeConfig,\n                $this->$typeMethod()\n            )\n        );\n    }\n\n    /**\n     * isPathIgnored determines if a path should be ignored\n     * @param string $path\n     * @return boolean\n     */\n    public static function isPathIgnored($path)\n    {\n        $ignoreNames = self::get('ignore_files');\n        $ignorePatterns = self::get('ignore_patterns');\n\n        if (in_array($path, $ignoreNames)) {\n            return true;\n        }\n\n        foreach ($ignorePatterns as $pattern) {\n            if (preg_match('/'.$pattern.'/', $path)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * ignoreFiles that can be safely ignored.\n     * This list can be customized with config:\n     * - media.ignore_files\n     */\n    protected function ignoreFiles()\n    {\n        return [\n            '.svn',\n            '.git',\n            '.DS_Store',\n            '.AppleDouble'\n        ];\n    }\n\n    /**\n     * ignorePatterns that can be safely ignored.\n     * This list can be customized with config:\n     * - media.ignore_patterns\n     */\n    protected function ignorePatterns()\n    {\n        return [\n            '^\\..*'\n        ];\n    }\n\n    /**\n     * defaultExtensions that are particularly benign.\n     * This list can be customized with config:\n     * - media.default_extensions\n     */\n    protected function defaultExtensions()\n    {\n        return [\n            'jpg',\n            'jpeg',\n            'bmp',\n            'png',\n            'webp',\n            'avif',\n            'gif',\n            'svg',\n            'js',\n            'map',\n            'ico',\n            'css',\n            'less',\n            'scss',\n            'ics',\n            'odt',\n            'doc',\n            'docx',\n            'ppt',\n            'pptx',\n            'pdf',\n            'swf',\n            'txt',\n            'ods',\n            'xls',\n            'xlsx',\n            'eot',\n            'woff',\n            'woff2',\n            'ttf',\n            'flv',\n            'wmv',\n            'mp3',\n            'ogg',\n            'wav',\n            'avi',\n            'mov',\n            'mp4',\n            'mpeg',\n            'webm',\n            'mkv',\n            'rar',\n            'zip'\n        ];\n    }\n\n    /**\n     * assetExtensions seen as public assets.\n     * This list can be customized with config:\n     * - media.asset_extensions\n     */\n    protected function assetExtensions()\n    {\n        return [\n            'jpg',\n            'jpeg',\n            'bmp',\n            'png',\n            'webp',\n            'avif',\n            'gif',\n            'ico',\n            'css',\n            'js',\n            'woff',\n            'woff2',\n            'svg',\n            'ttf',\n            'eot',\n            'json',\n            'md',\n            'less',\n            'sass',\n            'scss'\n        ];\n    }\n\n    /**\n     * imageExtensions typically used as images.\n     * This list can be customized with config:\n     * - media.image_extensions\n     */\n    protected function imageExtensions()\n    {\n        return [\n            'jpg',\n            'jpeg',\n            'bmp',\n            'png',\n            'webp',\n            'avif',\n            'gif'\n        ];\n    }\n\n    /**\n     * videoExtensions typically used as video files.\n     * This list can be customized with config:\n     * - media.video_extensions\n     */\n    protected function videoExtensions()\n    {\n        return [\n            'mp4',\n            'avi',\n            'mov',\n            'mpg',\n            'mpeg',\n            'mkv',\n            'webm'\n        ];\n    }\n\n    /**\n     * audioExtensions typically used as audio files.\n     * This list can be customized with config:\n     * - media.audio_extensions\n     */\n    protected function audioExtensions()\n    {\n        return [\n            'mp3',\n            'wav',\n            'wma',\n            'm4a',\n            'ogg'\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Filesystem/Filesystem.php",
    "content": "<?php namespace October\\Rain\\Filesystem;\n\nuse Event;\nuse ReflectionClass;\nuse FilesystemIterator;\nuse Illuminate\\Filesystem\\Filesystem as FilesystemBase;\n\n/**\n * Filesystem helper\n *\n * @package october\\filesystem\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Filesystem extends FilesystemBase\n{\n    /**\n     * @var string Default file permission mask as a string (\"755\").\n     */\n    public $filePermissions = null;\n\n    /**\n     * @var string Default folder permission mask as a string (\"755\").\n     */\n    public $folderPermissions = null;\n\n    /**\n     * @var array Known path symbols and their prefixes.\n     */\n    public $pathSymbols = [];\n\n    /**\n     * @var array|null A cache of symlinked root directories.\n     */\n    protected $symlinkRootCache;\n\n    /**\n     * anyname extracts the path and filename without extension\n     * @param  string  $path\n     * @return string\n     */\n    public function anyname($path)\n    {\n        return strpos(basename($path), '.') !== false ? substr($path, 0, strrpos($path, '.')) : $path;\n    }\n\n    /**\n     * isDirectoryEmpty determines if the given path contains no files\n     * @param  string  $directory\n     * @return bool\n     */\n    public function isDirectoryEmpty($directory)\n    {\n        if (!is_readable($directory)) {\n            return null;\n        }\n\n        $handle = opendir($directory);\n        while (false !== ($entry = readdir($handle))) {\n            if ($entry !== '.' && $entry !== '..') {\n                closedir($handle);\n                return false;\n            }\n        }\n\n        closedir($handle);\n        return true;\n    }\n\n    /**\n     * sizeToString converts a file size in bytes to human readable format\n     * @param  int $bytes\n     * @return string\n     */\n    public function sizeToString($bytes)\n    {\n        if ($bytes >= 1073741824) {\n            return number_format($bytes / 1073741824, 2) . ' GB';\n        }\n\n        if ($bytes >= 1048576) {\n            return number_format($bytes / 1048576, 2) . ' MB';\n        }\n\n        if ($bytes >= 1024) {\n            return number_format($bytes / 1024, 2) . ' KB';\n        }\n\n        if ($bytes > 1) {\n            return $bytes . ' bytes';\n        }\n\n        if ($bytes === 1) {\n            return $bytes . ' byte';\n        }\n\n        return '0 bytes';\n    }\n\n    /**\n     * localToPublic returns a public file path from an absolute one\n     * eg: /home/mysite/public_html/welcome -> /welcome\n     * @param  string $path Absolute path\n     * @return string\n     */\n    public function localToPublic($path)\n    {\n        /**\n         * @event filesystem.localToPublic\n         * Allow custom logic for converting local to public paths on non-standard installations.\n         *\n         * Example usage\n         *\n         *     Event::listen('filesystem.localToPublic', function ($path) {\n         *         return '/custom/public/path';\n         *     });\n         */\n        if (($event = Event::fire('filesystem.localToPublic', [$path], true)) !== null) {\n            return $event;\n        }\n\n        // Check real paths\n        $basePath = base_path();\n        if (strpos($path, $basePath) === 0) {\n            return str_replace(\"\\\\\", \"/\", substr($path, strlen($basePath)));\n        }\n\n        // Check first level symlinks\n        foreach ($this->getRootSymlinks() as $dir) {\n            $resolvedDir = readlink($dir);\n            if (strpos($path, $resolvedDir) === 0) {\n                $relativePath = substr($path, strlen($resolvedDir));\n                return str_replace(\"\\\\\", \"/\", substr($dir, strlen($basePath)) . $relativePath);\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * getRootSymlinks returns any unresolved symlinks in the public directory\n     */\n    protected function getRootSymlinks(): array\n    {\n        if ($this->symlinkRootCache === null) {\n            $symDirs = [];\n\n            foreach ($this->directories(base_path()) as $dir) {\n                if (is_link($dir)) {\n                    $symDirs[] = $dir;\n                }\n            }\n\n            $this->symlinkRootCache = $symDirs;\n        }\n\n        return $this->symlinkRootCache;\n    }\n\n    /**\n     * isLocalPath returns true if the specified path is within the path of the application.\n     * realpath resolves the provided path before checking location, set to false if you need\n     * to check if a potentially non-existent path would be within the application path.\n     * @param  string  $path\n     * @param  bool $realpath\n     * @return bool\n     */\n    public function isLocalPath($path, $realpath = true)\n    {\n        $base = base_path();\n\n        if ($realpath) {\n            $path = realpath($path);\n        }\n\n        return !($path === false || strncmp($path, $base, strlen($base)) !== 0);\n    }\n\n    /**\n     * fromClass finds the path to a class\n     * @param  mixed  $className Class name or object\n     * @return string The file path\n     */\n    public function fromClass($className)\n    {\n        $reflector = new ReflectionClass($className);\n        return $reflector->getFileName();\n    }\n\n    /**\n     * existsInsensitive determines if a file exists with case insensitivity\n     * supported for the file only. Returne either the sensitive path or false.\n     * @param  string $path\n     * @return string|bool\n     */\n    public function existsInsensitive($path)\n    {\n        if ($this->exists($path)) {\n            return $path;\n        }\n\n        $directoryName = dirname($path);\n        $pathLower = strtolower($path);\n\n        if (!$files = $this->glob($directoryName . '/*', GLOB_NOSORT)) {\n            return false;\n        }\n\n        foreach ($files as $file) {\n            if (strtolower($file) === $pathLower) {\n                return $file;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * normalizePath returns a normalized version of the supplied path for use in\n     * combined Windows and Unix systems.\n     * @param  string $path\n     * @return string\n     */\n    public function normalizePath($path)\n    {\n        return str_replace('\\\\', '/', $path);\n    }\n\n    /**\n     * nicePath removes the base path from a local path and returns a relatively nice\n     * path that is suitable and safe for sharing.\n     * @param  string $path\n     * @return string\n     */\n    public function nicePath($path)\n    {\n        return $this->normalizePath(str_replace([\n            base_path(),\n            $this->normalizePath(base_path())\n        ], '~', $path));\n    }\n\n    /**\n     * symbolizePath converts a path using path symbol. Returns the original path if\n     * no symbol is used and no default is specified.\n     * @param  string $path\n     * @param  mixed $default\n     * @return string\n     */\n    public function symbolizePath($path, $default = false)\n    {\n        if (!$firstChar = $this->isPathSymbol($path)) {\n            return $default === false ? $path : $default;\n        }\n\n        $_path = substr($path, 1);\n        return $this->pathSymbols[$firstChar] . $_path;\n    }\n\n    /**\n     * isPathSymbol returns the symbol if the path uses a symbol, otherwise false\n     * @param  string  $path\n     * @return bool|string\n     */\n    public function isPathSymbol($path)\n    {\n        if (!$path) {\n            return false;\n        }\n\n        $firstChar = substr($path, 0, 1);\n        if (isset($this->pathSymbols[$firstChar])) {\n            return $firstChar;\n        }\n\n        return false;\n    }\n\n    /**\n     * put writes the contents of a file\n     * @param  string  $path\n     * @param  string  $contents\n     * @return int\n     */\n    public function put($path, $contents, $lock = false)\n    {\n        $result = parent::put($path, $contents, $lock);\n        $this->chmod($path);\n        return $result;\n    }\n\n    /**\n     * copy a file to a new location.\n     * @param  string  $path\n     * @param  string  $target\n     * @return bool\n     */\n    public function copy($path, $target)\n    {\n        $result = parent::copy($path, $target);\n        $this->chmod($target);\n        return $result;\n    }\n\n    /**\n     * getSafe reads the first portion of file contents\n     */\n    public function getSafe(string $path, float $limitKbs = 1)\n    {\n        $limit = $limitKbs * 4096;\n\n        $parser = fopen($path, 'r');\n\n        return fread($parser, $limit);\n    }\n\n    /**\n     * makeDirectory creates a directory\n     * @param  string  $path\n     * @param  int     $mode\n     * @param  bool    $recursive\n     * @param  bool    $force\n     * @return bool\n     */\n    public function makeDirectory($path, $mode = 0755, $recursive = false, $force = false)\n    {\n        if ($mask = $this->getFolderPermissions()) {\n            $mode = $mask;\n        }\n\n        // Find the green leaves\n        if ($recursive && $mask) {\n            $chmodPath = $path;\n            while (true) {\n                $basePath = dirname($chmodPath);\n                if ($chmodPath === $basePath) {\n                    break;\n                }\n                if ($this->isDirectory($basePath)) {\n                    break;\n                }\n                $chmodPath = $basePath;\n            }\n        }\n        else {\n            $chmodPath = $path;\n        }\n\n        // Make the directory\n        $result = parent::makeDirectory($path, $mode, $recursive, $force);\n\n        // Apply the permissions\n        if ($mask) {\n            $this->chmod($chmodPath, $mask);\n\n            if ($recursive) {\n                $this->chmodRecursive($chmodPath, null, $mask);\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * chmod modifies file/folder permissions\n     * @param  string $path\n     * @param  octal $mask\n     * @return void\n     */\n    public function chmod($path, $mask = null)\n    {\n        if (!$mask) {\n            $mask = $this->isDirectory($path)\n                ? $this->getFolderPermissions()\n                : $this->getFilePermissions();\n        }\n\n        if (!$mask) {\n            return;\n        }\n\n        return @chmod($path, $mask);\n    }\n\n    /**\n     * chmodRecursive modifies file/folder permissions recursively\n     * @param  string $path\n     * @param  octal $fileMask\n     * @param  octal $directoryMask\n     * @return void\n     */\n    public function chmodRecursive($path, $fileMask = null, $directoryMask = null)\n    {\n        if (!$fileMask) {\n            $fileMask = $this->getFilePermissions();\n        }\n\n        if (!$directoryMask) {\n            $directoryMask = $this->getFolderPermissions() ?: $fileMask;\n        }\n\n        if (!$fileMask) {\n            return;\n        }\n\n        if (!$this->isDirectory($path)) {\n            return $this->chmod($path, $fileMask);\n        }\n\n        $items = new FilesystemIterator($path, FilesystemIterator::SKIP_DOTS);\n        foreach ($items as $item) {\n            if ($item->isDir()) {\n                $_path = $item->getPathname();\n                $this->chmod($_path, $directoryMask);\n                $this->chmodRecursive($_path, $fileMask, $directoryMask);\n            }\n            else {\n                $this->chmod($item->getPathname(), $fileMask);\n            }\n        }\n    }\n\n    /**\n     * getFilePermissions returns the default file permission mask to use\n     * @return string Permission mask as octal (0755) or null\n     */\n    public function getFilePermissions()\n    {\n        return $this->filePermissions\n            ? octdec($this->filePermissions)\n            : null;\n    }\n\n    /**\n     * getFolderPermissions returns the default folder permission mask to use\n     * @return string Permission mask as octal (0755) or null\n     */\n    public function getFolderPermissions()\n    {\n        return $this->folderPermissions\n            ? octdec($this->folderPermissions)\n            : null;\n    }\n\n    /**\n     * fileNameMatch matches filename against a pattern\n     * @param  string|array $fileName\n     * @param  string $pattern\n     * @return bool\n     */\n    public function fileNameMatch($fileName, $pattern)\n    {\n        if ($pattern === $fileName) {\n            return true;\n        }\n\n        $regex = strtr(preg_quote($pattern, '#'), ['\\*' => '.*', '\\?' => '.']);\n\n        return (bool) preg_match('#^' . $regex . '$#i', $fileName);\n    }\n\n    /**\n     * lastModifiedRecursive checks an entire directory and\n     * returns the mtime of the freshest file.\n     */\n    public function lastModifiedRecursive($path)\n    {\n        $mtime = 0;\n\n        foreach ($this->allFiles($path) as $file) {\n            $mtime = max($mtime, $this->lastModified($file->getPathname()));\n        }\n\n        return $mtime;\n    }\n\n    /**\n     * searchDirectory locates a file and return its relative path Eg: Searching\n     * directory /home/mysite for file index.php could locate this file\n     * /home/mysite/public_html/welcome/index.php and would return\n     * public_html/welcome\n     * @param  string $file index.php\n     * @param  string $directory /home/mysite\n     * @return string public_html/welcome\n     */\n    public function searchDirectory($file, $directory, $rootDir = '')\n    {\n        $files = $this->files($directory);\n        $directories = $this->directories($directory);\n\n        foreach ($files as $directoryFile) {\n            if ($directoryFile->getFileName() === $file) {\n                return $rootDir;\n            }\n        }\n\n        foreach ($directories as $subdirectory) {\n            $relativePath = strlen($rootDir)\n                ? $rootDir.'/'.basename($subdirectory)\n                : basename($subdirectory);\n\n            $result = $this->searchDirectory($file, $subdirectory, $relativePath);\n            if ($result !== null) {\n                return $result;\n            }\n        }\n\n        return null;\n    }\n}\n"
  },
  {
    "path": "src/Filesystem/FilesystemServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Filesystem;\n\nuse Illuminate\\Filesystem\\FilesystemServiceProvider as FilesystemServiceProviderBase;\n\n/**\n * FilesystemServiceProvider\n */\nclass FilesystemServiceProvider extends FilesystemServiceProviderBase\n{\n    /**\n     * register the service provider.\n     */\n    public function register()\n    {\n        $this->registerCoreDisks($this->app['config']);\n\n        $this->registerNativeFilesystem();\n\n        $this->registerFlysystem();\n\n        // After registration\n        $this->app->booting(function () {\n            $this->configureDefaultPermissions($this->app['config'], $this->app['files']);\n        });\n    }\n\n    /**\n     * registerNativeFilesystem implementation.\n     */\n    protected function registerNativeFilesystem()\n    {\n        $this->app->singleton('files', function () {\n            $config = $this->app['config'];\n\n            $files = new Filesystem;\n            $files->filePermissions = $config->get('system.default_mask.file', null);\n            $files->folderPermissions = $config->get('system.default_mask.folder', null);\n            $files->pathSymbols = [\n                '~' => base_path()\n            ];\n\n            if ($this->app->has('path.themes')) {\n                $files->pathSymbols['#'] = themes_path();\n            }\n\n            if ($this->app->has('path.plugins')) {\n                $files->pathSymbols['$'] = plugins_path();\n            }\n\n            return $files;\n        });\n    }\n\n    /**\n     * registerCoreDisks ensures\n     */\n    protected function registerCoreDisks($config)\n    {\n        if ($config->get('filesystems.disks.uploads') === null) {\n            $config->set('filesystems.disks.uploads', [\n                'driver' => 'local',\n                'root' => storage_path('app/uploads'),\n                'url' => '/storage/app/uploads',\n                'throw' => false,\n            ]);\n        }\n\n        if ($config->get('filesystems.disks.media') === null) {\n            $config->set('filesystems.disks.media', [\n                'driver' => 'local',\n                'root' => storage_path('app/media'),\n                'url' => '/storage/app/media',\n                'visibility' => 'public',\n                'throw' => false,\n            ]);\n        }\n\n        if ($config->get('filesystems.disks.resources') === null) {\n            $config->set('filesystems.disks.resources', [\n                'driver' => 'local',\n                'root' => storage_path('app/resources'),\n                'url' => '/storage/app/resources',\n                'visibility' => 'public',\n                'throw' => false,\n            ]);\n        }\n    }\n\n    /**\n     * configureDefaultPermissions\n     */\n    protected function configureDefaultPermissions($config, $files)\n    {\n        if ($config->get('filesystems.disks.local.permissions.file.public') === null) {\n            $config->set('filesystems.disks.local.permissions.file.public', $files->getFilePermissions());\n        }\n\n        if ($config->get('filesystems.disks.local.permissions.dir.public') === null) {\n            $config->set('filesystems.disks.local.permissions.dir.public', $files->getFolderPermissions());\n        }\n    }\n}\n"
  },
  {
    "path": "src/Filesystem/README.md",
    "content": "Filesystem - an extension of illuminate\\filesystem\n=======\n"
  },
  {
    "path": "src/Filesystem/Zip.php",
    "content": "<?php namespace October\\Rain\\Filesystem;\n\n/**\n * Zip helper\n *\n * @package october\\filesystem\n * @author Alexey Bobkov, Samuel Georges\n *\n * Usage:\n *\n *   Zip::make('file.zip', '/some/path/*.php');\n *\n *   Zip::make('file.zip', function($zip) {\n *\n *       // Add all PHP files and directories\n *       $zip->add('/some/path/*.php');\n *\n *       // Do not include subdirectories, one level only\n *       $zip->add('/non/recursive/*', ['recursive' => false]);\n *\n *       // Add multiple paths\n *       $zip->add([\n *           '/collection/of/paths/*',\n *           '/a/single/file.php'\n *       ]);\n *\n *       // Add all INI files to a zip folder \"config\"\n *       $zip->folder('config', '/path/to/config/*.ini');\n *\n *       // Add multiple paths to a zip folder \"images\"\n *       $zip->folder('images', function($zip) {\n *           $zip->add('/my/gifs/*.gif', );\n *           $zip->add('/photo/reel/*.{png,jpg}', );\n *       });\n *\n *       // Remove these files/folders from the zip\n *       $zip->remove([\n *           '.htaccess',\n *           'config.php',\n *           'some/folder'\n *       ]);\n *\n *   });\n *\n *   Zip::extract('file.zip', '/destination/path');\n *\n */\n\nuse ZipArchive;\n\nclass Zip extends ZipArchive\n{\n    /**\n     * @var string folderPrefix\n     */\n    protected $folderPrefix = '';\n\n    /**\n     * @var array excludedFiles\n     */\n    protected $excludedFiles = [];\n\n    /**\n     * @var array excludedFolders\n     */\n    protected $excludedFolders = [];\n\n    /**\n     * extract an existing zip file.\n     * @param  string $source Path for the existing zip\n     * @param  string $destination Path to extract the zip files\n     * @param  array  $options\n     * @return bool\n     */\n    public static function extract($source, $destination, $options = [])\n    {\n        extract(array_merge([\n            'mask' => 0755\n        ], $options));\n\n        if (file_exists($destination) || mkdir($destination, $mask, true)) {\n            $zip = new ZipArchive;\n            if ($zip->open($source) === true) {\n                $zip->extractTo($destination);\n                $zip->close();\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * make creates a new empty zip file.\n     * @param  string $destination Path for the new zip\n     * @param  mixed  $source\n     * @param  array  $options\n     * @return self\n     */\n    public static function make($destination, $source, $options = [])\n    {\n        $zip = new self;\n        $zip->open($destination, ZIPARCHIVE::CREATE | ZipArchive::OVERWRITE);\n\n        if (is_string($source)) {\n            $zip->add($source, $options);\n        }\n        elseif (is_callable($source)) {\n            $source($zip);\n        }\n        elseif (is_array($source)) {\n            foreach ($source as $_source) {\n                $zip->add($_source, $options);\n            }\n        }\n\n        $zip->close();\n        return $zip;\n    }\n\n    /**\n     * add includes a source to the Zip\n     * @param mixed $source\n     * @param array $options\n     * @return self\n     */\n    public function add($source, $options = [])\n    {\n        /*\n         * A directory has been supplied, convert it to a useful glob\n         *\n         * The wildcard for including hidden files:\n         * - isn't hidden with an '.'\n         * - is hidden with a '.' but is followed by a non '.' character\n         * - starts with '..' but has at least one character after it\n         */\n        if (is_dir($source)) {\n            $includeHidden = isset($options['includeHidden']) && $options['includeHidden'];\n            $wildcard = $includeHidden ? '{*,.[!.]*,..?*}' : '*';\n            $source = implode('/', [dirname($source), basename($source), $wildcard]);\n        }\n\n        extract(array_merge([\n            'recursive' => true,\n            'includeHidden' => false,\n            'basedir' => dirname($source),\n            'baseglob' => basename($source)\n        ], $options));\n\n        if (is_file($source)) {\n            $files = [$source];\n            $recursive = false;\n        }\n        else {\n            $files = glob($source, GLOB_BRACE);\n            $folders = glob(dirname($source) . '/*', GLOB_ONLYDIR);\n        }\n\n        foreach ($files as $file) {\n            if (!is_file($file)) {\n                continue;\n            }\n\n            if ($this->isExcluded($file)) {\n                continue;\n            }\n\n            $localpath = $this->removePathPrefix($basedir.'/', dirname($file).'/');\n            $localfile = $this->folderPrefix . $localpath . basename($file);\n            $this->addFile($file, $localfile);\n        }\n\n        if (!$recursive) {\n            return $this;\n        }\n\n        foreach ($folders as $folder) {\n            if (!is_dir($folder)) {\n                continue;\n            }\n\n            if ($this->isExcluded($folder)) {\n                continue;\n            }\n\n            $localpath = $this->folderPrefix . $this->removePathPrefix($basedir.'/', $folder.'/');\n            $this->addEmptyDir($localpath);\n            $this->add($folder.'/'.$baseglob, array_merge($options, ['basedir' => $basedir]));\n        }\n\n        return $this;\n    }\n\n\n    /**\n     * folder creates a new folder inside the Zip and adds source files (optional)\n     * @param  string $name Folder name\n     * @param  mixed  $source\n     * @return self\n     */\n    public function folder($name, $source = null)\n    {\n        $prefix = $this->folderPrefix;\n        $this->addEmptyDir($prefix . $name);\n        if ($source === null) {\n            return $this;\n        }\n\n        $this->folderPrefix = $prefix . $name . '/';\n\n        if (is_string($source)) {\n            $this->add($source);\n        }\n        elseif (is_callable($source)) {\n            $source($this);\n        }\n        elseif (is_array($source)) {\n            foreach ($source as $_source) {\n                $this->add($_source);\n            }\n        }\n\n        $this->folderPrefix = $prefix;\n        return $this;\n    }\n\n    /**\n     * remove a file or folder from the zip collection.\n     * Does not support wildcards.\n     * @param  string $source\n     * @return self\n     */\n    public function remove($source)\n    {\n        if (is_array($source)) {\n            foreach ($source as $_source) {\n                $this->remove($_source);\n            }\n        }\n\n        if (!is_string($source)) {\n            return $this;\n        }\n\n        if (substr($source, 0, 1) === '/') {\n            $source = substr($source, 1);\n        }\n\n        for ($i = $this->numFiles - 1; $i >= 0; $i--) {\n            if (($stats = $this->statIndex($i)) === false) {\n                continue;\n            }\n\n            if (substr($stats['name'], 0, strlen($source)) === $source) {\n                $this->deleteIndex($i);\n            }\n        }\n\n        return $this;\n    }\n\n    /**\n     * exclude paths from inclusion in the zip file\n     */\n    public function exclude(array $paths): void\n    {\n        foreach ($paths as $path) {\n            if (is_file($path)) {\n                $this->excludedFiles[] = $path;\n            }\n            elseif (is_dir($path)) {\n                $this->excludedFolders[] = $path;\n            }\n        }\n    }\n\n    /**\n     * isExcluded checks if a path is excluded\n     */\n    public function isExcluded(string $path): bool\n    {\n        if ($normalPath = realpath($path)) {\n            if (is_dir($normalPath)) {\n                return in_array($normalPath, $this->excludedFolders);\n            }\n            elseif (is_file($normalPath)) {\n                return in_array($normalPath, $this->excludedFiles);\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * removePathPrefix removes a prefix from a path.\n     * @param  string $prefix /var/sites/\n     * @param  string $path /var/sites/moo/cow/\n     * @return string moo/cow/\n     */\n    protected function removePathPrefix($prefix, $path)\n    {\n        return (strpos($path, $prefix) === 0)\n            ? substr($path, strlen($prefix))\n            : $path;\n    }\n}\n"
  },
  {
    "path": "src/Flash/FlashBag.php",
    "content": "<?php namespace October\\Rain\\Flash;\n\nuse App;\nuse Illuminate\\Support\\MessageBag;\n\n/**\n * FlashBag for simple session based messages\n *\n * @package october\\flash\n * @author Alexey Bobkov, Samuel Georges\n */\nclass FlashBag extends MessageBag\n{\n    const INFO = 'info';\n    const ERROR = 'error';\n    const SUCCESS = 'success';\n    const WARNING = 'warning';\n\n    const SESSION_KEY = '_flash_oc';\n\n    /**\n     * @var array newMessages all of the newly registered messages.\n     */\n    protected $newMessages = [];\n\n    /**\n     * @var \\Session session instance.\n     */\n    protected $session;\n\n    /**\n     * __construct\n     */\n    public function __construct(array $messages = [])\n    {\n        parent::__construct($messages);\n\n        $this->session = App::make('session');\n\n        if ($this->session->has(self::SESSION_KEY)) {\n            $this->messages = $this->session->get(self::SESSION_KEY);\n        }\n\n        $this->purge();\n    }\n\n    /**\n     * check to see if any message is available.\n     * @return bool\n     */\n    public function check()\n    {\n        return $this->any();\n    }\n\n    /**\n     * all gets first message for every key in the bag.\n     * @param string|null $format\n     * @return array\n     */\n    public function all($format = null)\n    {\n        $all = [];\n\n        foreach ($this->messages as $key => $messages) {\n            $all[$key] = reset($messages);\n        }\n\n        $this->purge();\n\n        return $all;\n    }\n\n    /**\n     * messages\n     */\n    public function messages()\n    {\n        $messages = parent::messages();\n\n        $this->purge();\n\n        return $messages;\n    }\n\n    /**\n     * get all the flash messages of a given type.\n     * @param string $key\n     * @param string|null $format\n     * @return array\n     */\n    public function get($key, $format = null)\n    {\n        $message = parent::get($key, $format);\n\n        $this->purge();\n\n        return $message;\n    }\n\n    /**\n     * error gets or sets an error message\n     * @param string|null $message\n     * @return array|FlashBag\n     */\n    public function error($message = null)\n    {\n        if ($message === null) {\n            return $this->get(FlashBag::ERROR);\n        }\n\n        return $this->add(FlashBag::ERROR, $message);\n    }\n\n    /**\n     * Sets Gets / a success message\n     * @param string|null $message\n     * @return array|FlashBag\n     */\n    public function success($message = null)\n    {\n        if ($message === null) {\n            return $this->get(FlashBag::SUCCESS);\n        }\n\n        return $this->add(FlashBag::SUCCESS, $message);\n    }\n\n    /**\n     * Gets / Sets a warning message\n     * @param string|null $message\n     * @return array|FlashBag\n     */\n    public function warning($message = null)\n    {\n        if ($message === null) {\n            return $this->get(FlashBag::WARNING);\n        }\n\n        return $this->add(FlashBag::WARNING, $message);\n    }\n\n    /**\n     * Gets / Sets a information message\n     * @param string|null $message\n     * @return array|FlashBag\n     */\n    public function info($message = null)\n    {\n        if ($message === null) {\n            return $this->get(FlashBag::INFO);\n        }\n\n        return $this->add(FlashBag::INFO, $message);\n    }\n\n    /**\n     * Add a message to the bag and stores it in the session.\n     *\n     * @param  string  $key\n     * @param  string  $message\n     * @return \\October\\Rain\\Flash\\FlashBag\n     */\n    public function add($key, $message)\n    {\n        $this->newMessages[$key][] = $message;\n\n        $this->store();\n\n        return parent::add($key, $message);\n    }\n\n    /**\n     * store the flash data to the session.\n     */\n    public function store()\n    {\n        $this->session->put(self::SESSION_KEY, $this->newMessages);\n    }\n\n    /**\n     * forget removes an object with a specified key or erases the flash data.\n     * @param string $key Specifies a key to remove, optional\n     */\n    public function forget($key = null)\n    {\n        if ($key === null) {\n            $this->newMessages = $this->messages = [];\n            $this->purge();\n        }\n        else {\n            if (isset($this->messages[$key])) {\n                unset($this->messages[$key]);\n            }\n\n            if (isset($this->newMessages[$key])) {\n                unset($this->newMessages[$key]);\n            }\n\n            $this->store();\n        }\n    }\n\n    /**\n     * purge all flash data from the session.\n     */\n    public function purge()\n    {\n        $this->session->remove(self::SESSION_KEY);\n    }\n}\n"
  },
  {
    "path": "src/Flash/FlashServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Flash;\n\nuse Illuminate\\Support\\ServiceProvider;\nuse Illuminate\\Contracts\\Support\\DeferrableProvider;\n\nclass FlashServiceProvider extends ServiceProvider implements DeferrableProvider\n{\n    /**\n     * register the service provider.\n     */\n    public function register()\n    {\n        $this->app->singleton('flash', function () {\n            return new FlashBag;\n        });\n\n        $this->app->alias('flash', FlashBag::class);\n    }\n\n    /**\n     * provides gets the services provided by the provider\n     */\n    public function provides()\n    {\n        return ['flash', FlashBag::class];\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Application.php",
    "content": "<?php namespace October\\Rain\\Foundation;\n\nuse October\\Rain\\Support\\Str;\nuse October\\Rain\\Support\\Collection;\nuse October\\Rain\\Filesystem\\Filesystem;\nuse October\\Rain\\Events\\EventServiceProvider;\nuse October\\Rain\\Router\\RoutingServiceProvider;\nuse October\\Rain\\Foundation\\Providers\\LogServiceProvider;\nuse October\\Rain\\Foundation\\Providers\\ExecutionContextProvider;\nuse Illuminate\\Foundation\\Application as ApplicationBase;\nuse Illuminate\\Foundation\\AliasLoader;\nuse Illuminate\\Foundation\\PackageManifest;\nuse Illuminate\\Foundation\\ProviderRepository;\nuse Illuminate\\Log\\Context\\ContextServiceProvider;\nuse Carbon\\Laravel\\ServiceProvider as CarbonServiceProvider;\nuse Illuminate\\Support\\Env;\nuse Throwable;\nuse Closure;\nuse Error;\n\n/**\n * Application foundation class as an extension of Laravel\n */\nclass Application extends ApplicationBase\n{\n    /**\n     * @var string pluginsPath is the base path for plugins\n     */\n    protected $pluginsPath;\n\n    /**\n     * @var string themesPath is the base path for themes\n     */\n    protected $themesPath;\n\n    /**\n     * @var string cachePath is the base path for cache files\n     */\n    protected $cachePath;\n\n    /**\n     * Begin configuring a new Laravel application instance.\n     *\n     * @param  string|null  $basePath\n     * @return \\Illuminate\\Foundation\\Configuration\\ApplicationBuilder\n     */\n    public static function configure($basePath = null)\n    {\n        if (!is_string($basePath)) {\n            $basePath = static::inferBasePath();\n        }\n\n        return (new Configuration\\ApplicationBuilder(new static($basePath)))\n            ->withKernels()\n            ->withEvents()\n            ->withCommands()\n            ->withProviders();\n    }\n\n    /**\n     * registerBaseServiceProviders registers all of the base service providers\n     */\n    protected function registerBaseServiceProviders()\n    {\n        $this->register(new EventServiceProvider($this));\n\n        $this->register(new LogServiceProvider($this));\n\n        $this->register(new ContextServiceProvider($this));\n\n        $this->register(new RoutingServiceProvider($this));\n\n        $this->register(new ExecutionContextProvider($this));\n    }\n\n    /**\n     * bindPathsInContainer binds all of the application paths in the container\n     */\n    protected function bindPathsInContainer()\n    {\n        parent::bindPathsInContainer();\n\n        // Additional lang path check\n        if (is_dir($directory = $this->path('lang'))) {\n            $this->useLangPath($directory);\n        }\n\n        // October CMS paths\n        $this->instance('path.plugins', $this->pluginsPath());\n        $this->instance('path.themes', $this->themesPath());\n        $this->instance('path.cache', $this->cachePath());\n        $this->instance('path.temp', $this->tempPath());\n    }\n\n    /**\n     * publicPath gets the path to the public / web directory\n     * @return string\n     */\n    public function publicPath($path = '')\n    {\n        return $this->hasPublicFolder()\n                ? $this->joinPaths($this->basePath('public'), $path)\n                : $this->joinPaths($this->basePath, $path);\n    }\n\n    /**\n     * hasPublicFolder returns true if a public folder exists, initiated by october:mirror\n     */\n    public function hasPublicFolder()\n    {\n        return file_exists($this->basePath('public'));\n    }\n\n    /**\n     * cachePath return path for cache files\n     * @param string $path\n     * @return string\n     */\n    public function cachePath($path = '')\n    {\n        return $this->joinPaths($this->cachePath ?: $this->basePath('storage'), $path);\n    }\n\n    /**\n     * useCachePath sets path path for cache files\n     * @param string $path\n     * @return $this\n     */\n    public function useCachePath($path)\n    {\n        $this->cachePath = $path;\n\n        $this->instance('path.cache', $path);\n\n        $this->instance('path.temp', $path.DIRECTORY_SEPARATOR.'temp');\n\n        return $this;\n    }\n\n    /**\n     * pluginsPath returns path to location of plugins\n     * @param string $path\n     * @return string\n     */\n    public function pluginsPath($path = '')\n    {\n        return $this->joinPaths($this->pluginsPath ?: $this->basePath('plugins'), $path);\n    }\n\n    /**\n     * usePluginsPath sets path to location of plugins\n     * @param string $path\n     * @return $this\n     */\n    public function usePluginsPath($path)\n    {\n        $this->pluginsPath = $path;\n\n        $this->instance('path.plugins', $path);\n\n        return $this;\n    }\n\n    /**\n     * themesPath returns path to location of themes\n     * @param string $path\n     * @return string\n     */\n    public function themesPath($path = '')\n    {\n        return $this->joinPaths($this->themesPath ?: $this->basePath('themes'), $path);\n    }\n\n    /**\n     * useThemesPath sets path to location of themes\n     * @param string $path\n     * @return $this\n     */\n    public function useThemesPath($path)\n    {\n        $this->themesPath = $path;\n\n        $this->instance('path.themes', $path);\n\n        return $this;\n    }\n\n    /**\n     * tempPath returns path for storing temporary files.\n     * @param string $path\n     * @return string\n     */\n    public function tempPath($path = ''): string\n    {\n        return $this->joinPaths($this->cachePath('temp'), $path);\n    }\n\n    /**\n     * normalizeCachePath normalizes a relative or absolute path to a cache file.\n     * @param string $key\n     * @param string $default\n     * @return string\n     */\n    protected function normalizeCachePath($key, $default)\n    {\n        if (is_null($env = Env::get($key))) {\n            return $this->cachePath($default);\n        }\n\n        return Str::startsWith($env, '/')\n            ? $env\n            : $this->basePath($env);\n    }\n\n    /**\n     * before logic is called before the router runs.\n     * @param  \\Closure|string  $callback\n     * @return void\n     */\n    public function before($callback)\n    {\n        return $this['router']->before($callback);\n    }\n\n    /**\n     * after logic is called after the router finishes.\n     * @param  \\Closure|string  $callback\n     * @return void\n     */\n    public function after($callback)\n    {\n        return $this['router']->after($callback);\n    }\n\n    /**\n     * error registers an application error handler.\n     * @param  \\Closure  $callback\n     * @return void\n     */\n    public function error(callable $callback)\n    {\n        $this->make(\\Illuminate\\Contracts\\Debug\\ExceptionHandler::class)->renderable($callback);\n    }\n\n    /**\n     * @deprecated use App::error with an Error exception type\n     */\n    public function fatal(callable $callback)\n    {\n        $this->error(function(Error $e) use ($callback) {\n            return $callback($e);\n        });\n    }\n\n    /**\n     * runningInBackend determines if we are running in the backend area.\n     * @return bool\n     */\n    public function runningInBackend()\n    {\n        return $this['execution.context'] === 'backend';\n    }\n\n    /**\n     * runningInFrontend determines if we are running in the frontend area.\n     * @return bool\n     */\n    public function runningInFrontend()\n    {\n        return !$this->runningInBackend() && !$this->runningInConsole();\n    }\n\n    /**\n     * runningInOctane determines if the application is running under Laravel Octane.\n     * @return bool\n     */\n    public function runningInOctane()\n    {\n        return isset($_ENV['OCTANE_SERVER']) && $_ENV['OCTANE_SERVER'];\n    }\n\n    /**\n     * hasDatabase returns true if a database connection is present.\n     * @return boolean\n     */\n    public function hasDatabase()\n    {\n        try {\n            $this['db.connection']->getPdo();\n        }\n        catch (Throwable $ex) {\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * setLocale for the application.\n     * @param  string  $locale\n     * @return void\n     */\n    public function setLocale($locale)\n    {\n        parent::setLocale($locale);\n\n        $this['events']->dispatch('locale.changed', [$locale]);\n    }\n\n    //\n    // Core registrations\n    //\n\n    /**\n     * registerConfiguredProviders is entirely inherited from the parent,\n     * except the October\\Rain namespace is included in the partition.\n     */\n    public function registerConfiguredProviders()\n    {\n        $providers = (new Collection($this->make('config')->get('app.providers')))\n            ->partition(function ($provider) {\n                return strpos($provider, 'Illuminate\\\\') === 0 ||\n                    strpos($provider, 'October\\\\Rain\\\\') === 0;\n            });\n\n        $providers->splice(1, 0, [$this->make(PackageManifest::class)->providers()]);\n\n        (new ProviderRepository($this, new Filesystem, $this->getCachedServicesPath()))\n            ->load($providers->collapse()->toArray());\n\n        $this->fireAppCallbacks($this->registeredCallbacks);\n    }\n\n    /**\n     * registerCoreContainerAliases in the container.\n     */\n    public function registerCoreContainerAliases()\n    {\n        $aliases = [\n            // These are introduced externally, for example, RainLab.User plugin\n            // 'auth' => [\\Illuminate\\Auth\\AuthManager::class, \\Illuminate\\Contracts\\Auth\\Factory::class],\n            // 'auth.driver' => [\\Illuminate\\Contracts\\Auth\\Guard::class],\n            'app' => [\\October\\Rain\\Foundation\\Application::class, \\Illuminate\\Foundation\\Application::class, \\Illuminate\\Contracts\\Container\\Container::class, \\Illuminate\\Contracts\\Foundation\\Application::class, \\Psr\\Container\\ContainerInterface::class],\n            'blade.compiler' => [\\Illuminate\\View\\Compilers\\BladeCompiler::class],\n            'cache' => [\\Illuminate\\Cache\\CacheManager::class, \\Illuminate\\Contracts\\Cache\\Factory::class],\n            'cache.store' => [\\Illuminate\\Cache\\Repository::class, \\Illuminate\\Contracts\\Cache\\Repository::class, \\Psr\\SimpleCache\\CacheInterface::class],\n            'cache.psr6' => [\\Symfony\\Component\\Cache\\Adapter\\Psr16Adapter::class, \\Symfony\\Component\\Cache\\Adapter\\AdapterInterface::class, \\Psr\\Cache\\CacheItemPoolInterface::class],\n            'config' => [\\Illuminate\\Config\\Repository::class, \\Illuminate\\Contracts\\Config\\Repository::class],\n            'cookie' => [\\Illuminate\\Cookie\\CookieJar::class, \\Illuminate\\Contracts\\Cookie\\Factory::class, \\Illuminate\\Contracts\\Cookie\\QueueingFactory::class],\n            'db' => [\\Illuminate\\Database\\DatabaseManager::class, \\Illuminate\\Database\\ConnectionResolverInterface::class],\n            'db.connection' => [\\Illuminate\\Database\\Connection::class, \\Illuminate\\Database\\ConnectionInterface::class],\n            'db.schema' => [\\Illuminate\\Database\\Schema\\Builder::class],\n            'encrypter' => [\\Illuminate\\Encryption\\Encrypter::class, \\Illuminate\\Contracts\\Encryption\\Encrypter::class, \\Illuminate\\Contracts\\Encryption\\StringEncrypter::class],\n            'events' => [\\October\\Rain\\Events\\Dispatcher::class, \\Illuminate\\Events\\Dispatcher::class, \\Illuminate\\Contracts\\Events\\Dispatcher::class],\n            'files' => [\\October\\Rain\\Filesystem\\Filesystem::class, \\Illuminate\\Filesystem\\Filesystem::class],\n            'filesystem' => [\\Illuminate\\Filesystem\\FilesystemManager::class, \\Illuminate\\Contracts\\Filesystem\\Factory::class],\n            'filesystem.disk' => [\\Illuminate\\Contracts\\Filesystem\\Filesystem::class],\n            'filesystem.cloud' => [\\Illuminate\\Contracts\\Filesystem\\Cloud::class],\n            'hash' => [\\Illuminate\\Hashing\\HashManager::class],\n            'hash.driver' => [\\Illuminate\\Contracts\\Hashing\\Hasher::class],\n            'translator' => [\\Illuminate\\Translation\\Translator::class, \\Illuminate\\Contracts\\Translation\\Translator::class],\n            'log' => [\\Illuminate\\Log\\LogManager::class, \\Psr\\Log\\LoggerInterface::class],\n            'mail.manager' => [\\Illuminate\\Mail\\MailManager::class, \\Illuminate\\Contracts\\Mail\\Factory::class],\n            'mailer' => [\\Illuminate\\Mail\\Mailer::class, \\Illuminate\\Contracts\\Mail\\Mailer::class, \\Illuminate\\Contracts\\Mail\\MailQueue::class],\n            'auth.password' => [\\Illuminate\\Auth\\Passwords\\PasswordBrokerManager::class, \\Illuminate\\Contracts\\Auth\\PasswordBrokerFactory::class],\n            'auth.password.broker' => [\\Illuminate\\Auth\\Passwords\\PasswordBroker::class, \\Illuminate\\Contracts\\Auth\\PasswordBroker::class],\n            'queue' => [\\Illuminate\\Queue\\QueueManager::class, \\Illuminate\\Contracts\\Queue\\Factory::class, \\Illuminate\\Contracts\\Queue\\Monitor::class],\n            'queue.connection' => [\\Illuminate\\Contracts\\Queue\\Queue::class],\n            'queue.failer' => [\\Illuminate\\Queue\\Failed\\FailedJobProviderInterface::class],\n            'redirect' => [\\October\\Rain\\Router\\CoreRedirector::class, \\Illuminate\\Routing\\Redirector::class],\n            'redis' => [\\Illuminate\\Redis\\RedisManager::class, \\Illuminate\\Contracts\\Redis\\Factory::class],\n            'redis.connection' => [\\Illuminate\\Redis\\Connections\\Connection::class, \\Illuminate\\Contracts\\Redis\\Connection::class],\n            'request' => [\\Illuminate\\Http\\Request::class, \\Symfony\\Component\\HttpFoundation\\Request::class],\n            'router' => [\\Illuminate\\Routing\\Router::class, \\Illuminate\\Contracts\\Routing\\Registrar::class, \\Illuminate\\Contracts\\Routing\\BindingRegistrar::class],\n            'session' => [\\Illuminate\\Session\\SessionManager::class],\n            'session.store' => [\\Illuminate\\Session\\Store::class, \\Illuminate\\Contracts\\Session\\Session::class],\n            'url' => [\\Illuminate\\Routing\\UrlGenerator::class, \\Illuminate\\Contracts\\Routing\\UrlGenerator::class],\n            'validator' => [\\October\\Rain\\Validation\\Factory::class, \\Illuminate\\Contracts\\Validation\\Factory::class],\n            'view' => [\\Illuminate\\View\\Factory::class, \\Illuminate\\Contracts\\View\\Factory::class],\n        ];\n\n        foreach ($aliases as $key => $aliases) {\n            foreach ($aliases as $alias) {\n                $this->alias($key, $alias);\n            }\n        }\n    }\n\n    /**\n     * registerClassAlias registers a new global alias, useful for facades\n     */\n    public function registerClassAlias(string $alias, string $class)\n    {\n        $this->registerClassAliases([$alias => $class]);\n    }\n\n    /**\n     * registerClassAliases registers multiple global aliases, useful for renamed classes\n     */\n    public function registerClassAliases(array $aliases)\n    {\n        AliasLoader::getInstance($aliases);\n    }\n\n    //\n    // Caching\n    //\n\n    /**\n     * Get the path to the configuration cache file.\n     *\n     * @return string\n     */\n    public function getCachedConfigPath()\n    {\n        return $this->normalizeCachePath('APP_CONFIG_CACHE', 'framework/config.php');\n    }\n\n    /**\n     * Get the path to the routes cache file.\n     *\n     * @return string\n     */\n    public function getCachedRoutesPath()\n    {\n        return $this->normalizeCachePath('APP_ROUTES_CACHE', 'framework/routes.php');\n    }\n\n    /**\n     * Get the path to the cached \"compiled.php\" file.\n     *\n     * @return string\n     */\n    public function getCachedCompilePath()\n    {\n        return $this->normalizeCachePath('APP_COMPILED_CACHE', 'framework/compiled.php');\n    }\n\n    /**\n     * Get the path to the cached services.json file.\n     *\n     * @return string\n     */\n    public function getCachedServicesPath()\n    {\n        return $this->normalizeCachePath('APP_SERVICES_CACHE', 'framework/services.php');\n    }\n\n    /**\n     * Get the path to the cached packages.php file.\n     *\n     * @return string\n     */\n    public function getCachedPackagesPath()\n    {\n        return $this->normalizeCachePath('APP_PACKAGES_CACHE', 'framework/packages.php');\n    }\n\n    /**\n     * Get the path to the cached packages.php file.\n     *\n     * @return string\n     */\n    public function getCachedClassesPath()\n    {\n        return $this->normalizeCachePath('APP_CLASSES_CACHE', 'framework/classes.php');\n    }\n\n    /**\n     * Get the path to the events cache file.\n     *\n     * @return string\n     */\n    public function getCachedEventsPath()\n    {\n        return $this->normalizeCachePath('APP_EVENTS_CACHE', 'framework/events.php');\n    }\n\n    /**\n     * getNamespace returns the application namespace.\n     * @return string\n     * @throws \\RuntimeException\n     */\n    public function getNamespace()\n    {\n        return 'App\\\\';\n    }\n\n    /**\n     * extendInstance is useful for extending singletons regardless of their execution\n     */\n    public function extendInstance($abstract, Closure $callback)\n    {\n        $this->afterResolving($abstract, $callback);\n\n        if ($this->resolved($abstract)) {\n            $callback($this->make($abstract), $this);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Bootstrap/HandleExceptions.php",
    "content": "<?php namespace October\\Rain\\Foundation\\Bootstrap;\n\nuse Config;\nuse Illuminate\\Foundation\\Bootstrap\\HandleExceptions as HandleExceptionsBase;\n\n/**\n * HandleExceptions is a registration point for handling exceptions\n */\nclass HandleExceptions extends HandleExceptionsBase\n{\n    /**\n     * shouldIgnoreDeprecationErrors determine if deprecation errors should be ignored.\n     *\n     * The logic here used by Laravel is unsatisfactory since the MessageLogged event\n     * won't distinguish between exceptions and deprecations or the log driver used,\n     * so a custom configuration key is included as part of the core system.\n     *\n     * @return bool\n     */\n    protected function shouldIgnoreDeprecationErrors()\n    {\n        if (Config::get('system.log_deprecations', false) === false) {\n            return true;\n        }\n\n        return parent::shouldIgnoreDeprecationErrors();\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Bootstrap/LoadConfiguration.php",
    "content": "<?php namespace October\\Rain\\Foundation\\Bootstrap;\n\nuse October\\Rain\\Config\\Repository;\nuse Illuminate\\Contracts\\Foundation\\Application;\nuse Illuminate\\Foundation\\Bootstrap\\LoadConfiguration as LoadConfigurationBase;\nuse Illuminate\\Contracts\\Config\\Repository as RepositoryContract;\nuse Exception;\n\n/**\n * LoadConfiguration bootstraps the configuration instance\n */\nclass LoadConfiguration extends LoadConfigurationBase\n{\n    /**\n     * bootstrap the given application.\n     */\n    public function bootstrap(Application $app)\n    {\n        $items = [];\n\n        // First we will see if we have a cache configuration file. If we do, we'll load\n        // the configuration items from that file so that it is very quick. Otherwise\n        // we will need to spin through every configuration file and load them all.\n        if (file_exists($cached = $app->getCachedConfigPath())) {\n            $items = require $cached;\n\n            $app->instance('config_loaded_from_cache', $loadedFromCache = true);\n        }\n\n        // Next we will spin through all of the configuration files in the configuration\n        // directory and load each one into the repository. This will make all of the\n        // options available to the developer for use in various parts of this app.\n        $app->instance('config', $config = new Repository($items));\n\n        if (!isset($loadedFromCache)) {\n            $this->loadConfigurationFiles($app, $config);\n        }\n\n        // Finally, we will set the application's environment based on the configuration\n        // values that were loaded. We will pass a callback which will be used to get\n        // the environment in a web context where an \"--env\" switch is not present.\n        $app->detectEnvironment(function () use ($config) {\n            return $config->get('app.env', 'production');\n        });\n\n        date_default_timezone_set($config->get('app.timezone', 'UTC'));\n\n        mb_internal_encoding('UTF-8');\n\n        // Fix for XDebug aborting threads > 100 nested\n        ini_set('xdebug.max_nesting_level', 1000);\n    }\n\n    /**\n     * loadConfigurationFiles from all of the files.\n     *\n     * @param  \\Illuminate\\Contracts\\Foundation\\Application  $app\n     * @param  \\Illuminate\\Contracts\\Config\\Repository  $repository\n     * @return void\n     *\n     * @throws \\Exception\n     */\n    protected function loadConfigurationFiles(Application $app, RepositoryContract $repository)\n    {\n        $files = $this->getConfigurationFiles($app);\n\n        if (!isset($files['app'])) {\n            throw new Exception('Unable to load the \"app\" configuration file.');\n        }\n\n        foreach ($files as $key => $path) {\n            // Filenames with config.php are treated as root nodes\n            if (basename($path) === 'config.php') {\n                $key = substr($key, 0, -7);\n            }\n\n            $repository->set($key, require $path);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Bootstrap/RegisterOctober.php",
    "content": "<?php namespace October\\Rain\\Foundation\\Bootstrap;\n\nuse October\\Rain\\Support\\Str;\nuse October\\Rain\\Composer\\ClassLoader;\nuse October\\Rain\\Support\\ServiceProvider;\nuse Illuminate\\Contracts\\Foundation\\Application;\nuse October\\Rain\\Extension\\Container as OctoberContainer;\n\n/**\n * RegisterOctober specific features\n */\nclass RegisterOctober\n{\n    /**\n     * cachePaths used by the system\n     */\n    protected $cachePaths = [\n        'cms',\n        'cms/cache',\n        'cms/combiner',\n        'cms/twig',\n        'framework',\n        'framework/cache',\n        'framework/views',\n        'temp',\n        'temp/public',\n    ];\n\n    /**\n     * storagePaths used by the system\n     */\n    protected $storagePaths = [\n        'app',\n        'app/media',\n        'app/uploads',\n        'framework',\n        'framework/sessions',\n        'logs',\n    ];\n\n    /**\n     * bootstrap the application\n     */\n    public function bootstrap(Application $app)\n    {\n        // Swap out for October's default providers\n        // This must happen before RegisterProviders runs\n        $app->make('config')->set(\n            'app.providers',\n            $app->make('config')->get('app.providers') ?? ServiceProvider::defaultProviders()->toArray(),\n        );\n\n        // Register singletons\n        $app->singleton('string', function () {\n            return new \\October\\Rain\\Support\\Str;\n        });\n\n        // Change paths based on config\n        if ($storagePath = $app['config']->get('system.storage_path')) {\n            $app->useStoragePath($this->parseConfiguredPath($app, $storagePath));\n        }\n\n        if ($cachePath = $app['config']->get('system.cache_path')) {\n            $app->useCachePath($this->parseConfiguredPath($app, $cachePath));\n        }\n\n        if ($pluginsPath = $app['config']->get('system.plugins_path')) {\n            $app->usePluginsPath($this->parseConfiguredPath($app, $pluginsPath));\n        }\n\n        if ($themesPath = $app['config']->get('system.themes_path')) {\n            $app->useThemesPath($this->parseConfiguredPath($app, $themesPath));\n        }\n\n        // Make system paths\n        if ($app->cachePath() === $app->storagePath()) {\n            $this->makeSystemPaths($app->cachePath(), array_unique(\n                array_merge($this->cachePaths, $this->storagePaths)\n            ));\n        }\n        else {\n            $this->makeSystemPaths($app->cachePath(), $this->cachePaths);\n            $this->makeSystemPaths($app->storagePath(), $this->storagePaths);\n        }\n\n        // Configure the custom class loader\n        $this->configureClassLoader($app);\n\n        // Clear service container\n        OctoberContainer::clearExtensions();\n    }\n\n    /**\n     * configureClassLoader initializes the class loader cache\n     */\n    protected function configureClassLoader(Application $app)\n    {\n        $loader = ClassLoader::instance();\n\n        $loader->initManifest($app->getCachedClassesPath());\n\n        $app->after(function () use ($loader) {\n            $loader->build();\n        });\n    }\n\n    /**\n     * parseConfiguredPath will include the base path if necessary\n     */\n    protected function parseConfiguredPath(Application $app, string $path): string\n    {\n        return Str::startsWith($path, '/')\n            ? $path\n            : $app->basePath($path);\n    }\n\n    /**\n     * makeSystemPaths will attempt to ensure the required system paths exist\n     */\n    protected function makeSystemPaths(string $rootPath, array $subPaths): void\n    {\n        if (file_exists($rootPath)) {\n            return;\n        }\n\n        @mkdir($rootPath);\n\n        foreach ($subPaths as $path) {\n            $subPath = $rootPath.DIRECTORY_SEPARATOR.$path;\n            if (file_exists($subPath)) {\n                continue;\n            }\n\n            @mkdir($subPath);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Configuration/ApplicationBuilder.php",
    "content": "<?php namespace October\\Rain\\Foundation\\Configuration;\n\nuse Illuminate\\Foundation\\Configuration\\ApplicationBuilder as ApplicationBuilderBase;\nuse Illuminate\\Foundation\\Configuration\\Exceptions;\n\n/**\n * ApplicationBuilder foundation class as an extension of Laravel\n */\nclass ApplicationBuilder extends ApplicationBuilderBase\n{\n    /**\n     * Register the standard kernel classes for the application.\n     *\n     * @return $this\n     */\n    public function withKernels()\n    {\n        $this->app->singleton(\n            \\Illuminate\\Contracts\\Http\\Kernel::class,\n            \\October\\Rain\\Foundation\\Http\\Kernel::class\n        );\n\n        $this->app->singleton(\n            \\Illuminate\\Contracts\\Console\\Kernel::class,\n            \\October\\Rain\\Foundation\\Console\\Kernel::class\n        );\n\n        return $this;\n    }\n\n    /**\n     * Register and configure the application's exception handler.\n     *\n     * @param  callable|null  $using\n     * @return $this\n     */\n    public function withExceptions(?callable $using = null)\n    {\n        $this->app->singleton(\n            \\Illuminate\\Contracts\\Debug\\ExceptionHandler::class,\n            \\October\\Rain\\Foundation\\Exception\\Handler::class\n        );\n\n        $using ??= fn () => true;\n\n        $this->app->afterResolving(\n            \\Illuminate\\Foundation\\Exceptions\\Handler::class,\n            fn ($handler) => $using(new Exceptions($handler)),\n        );\n\n        return $this;\n    }\n\n    /**\n     * withMiddleware is a modifier to the parent logic to remove unwanted default middleware\n     */\n    public function withMiddleware(?callable $callback = null)\n    {\n        $nested = function($middleware) use ($callback) {\n            $middleware\n                ->remove([\n                    \\Illuminate\\Foundation\\Http\\Middleware\\ConvertEmptyStringsToNull::class,\n                    \\Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance::class,\n                ])\n                ->append([\n                    \\October\\Rain\\Foundation\\Http\\Middleware\\CheckForMaintenanceMode::class,\n                ])\n                ->removeFromGroup('web', [\n                    \\Illuminate\\Cookie\\Middleware\\EncryptCookies::class,\n                    \\Illuminate\\Foundation\\Http\\Middleware\\ValidateCsrfToken::class,\n                ])\n                ->appendToGroup('web', [\n                    \\October\\Rain\\Foundation\\Http\\Middleware\\EncryptCookies::class,\n                ]);\n\n            if ($callback !== null) {\n                $callback($middleware);\n            }\n        };\n\n        return parent::withMiddleware($nested);\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Console/ClearCompiledCommand.php",
    "content": "<?php namespace October\\Rain\\Foundation\\Console;\n\nuse Illuminate\\Foundation\\Console\\ClearCompiledCommand as ClearCompiledCommandBase;\n\nclass ClearCompiledCommand extends ClearCompiledCommandBase\n{\n    /**\n     * Execute the console command.\n     *\n     * @return void\n     */\n    public function handle()\n    {\n        if (file_exists($classesPath = $this->laravel->getCachedClassesPath())) {\n            @unlink($classesPath);\n        }\n\n        parent::handle();\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Console/Kernel.php",
    "content": "<?php namespace October\\Rain\\Foundation\\Console;\n\nuse Illuminate\\Console\\Scheduling\\Schedule;\nuse Illuminate\\Foundation\\Console\\Kernel as ConsoleKernel;\n\n/**\n * Kernel\n */\nclass Kernel extends ConsoleKernel\n{\n    /**\n     * @var array bootstrappers for the application\n     */\n    protected $bootstrappers = [\n        \\Illuminate\\Foundation\\Bootstrap\\LoadEnvironmentVariables::class,\n        \\October\\Rain\\Foundation\\Bootstrap\\LoadConfiguration::class,\n        \\October\\Rain\\Foundation\\Bootstrap\\HandleExceptions::class,\n        \\Illuminate\\Foundation\\Bootstrap\\RegisterFacades::class,\n        \\Illuminate\\Foundation\\Bootstrap\\SetRequestForConsole::class,\n        \\October\\Rain\\Foundation\\Bootstrap\\RegisterOctober::class,\n        \\Illuminate\\Foundation\\Bootstrap\\RegisterProviders::class,\n        \\Illuminate\\Foundation\\Bootstrap\\BootProviders::class,\n    ];\n\n    /**\n     * @var array commands provided by your application\n     */\n    protected $commands = [];\n\n    /**\n     * schedule defines the application's command schedule\n     */\n    protected function schedule(Schedule $schedule)\n    {\n        $this->bootstrap();\n\n        $this->app['events']->dispatch('console.schedule', [$schedule]);\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Console/ProjectSetCommand.php",
    "content": "<?php namespace October\\Rain\\Foundation\\Console;\n\nuse Url;\nuse Http;\nuse Config;\nuse Illuminate\\Console\\Command;\nuse October\\Rain\\Composer\\ComposerManager;\nuse Exception;\n\n/**\n * ProjectSetCommand the project license key.\n *\n * @package october\\system\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ProjectSetCommand extends Command\n{\n    /**\n     * @var string signature of console command\n     */\n    protected $signature = 'project:set {key?}';\n\n    /**\n     * The console command description.\n     */\n    protected $description = 'Sets the project license key.';\n\n    /**\n     * Execute the console command.\n     */\n    public function handle()\n    {\n        $projectKey = (string) $this->argument('key');\n\n        if (!$projectKey) {\n            $this->comment(__(\"Enter a valid License Key to proceed.\"));\n\n            $projectKey = trim($this->ask(__(\"License Key\")));\n        }\n\n        try {\n            // Validate input with gateway\n            $result = $this->requestServerData('project/detail', ['id' => $projectKey]);\n\n            // Check project status\n            $isActive = $result['is_active'] ?? false;\n            if (!$isActive) {\n                $this->output->error(__(\"License is unpaid or has expired. Please visit octobercms.com to obtain a license.\"));\n                return;\n            }\n\n            // Store project details\n            $this->storeProjectDetails($result);\n\n            // Add gateway as a composer repo\n            ComposerManager::instance()->addOctoberRepository($this->getComposerUrl());\n\n            $this->output->success(__(\"Thanks for being a customer of October CMS!\"));\n        }\n        catch (Exception $e) {\n            $this->output->error($e->getMessage());\n            return 1;\n        }\n    }\n\n    /**\n     * storeProjectDetails\n     */\n    protected function storeProjectDetails($result)\n    {\n        // Save project locally\n        try {\n            if (class_exists(\\System\\Models\\Parameter::class)) {\n                \\System\\Models\\Parameter::set([\n                    'system::project.id' => $result['id'],\n                    'system::project.key' => $result['project_id'],\n                    'system::project.name' => $result['name'],\n                    'system::project.owner' => $result['owner'],\n                    'system::project.is_active' => $result['is_active']\n                ]);\n            }\n            else {\n               $this->storeProjectDetailsLocally($result);\n            }\n        }\n        catch (Exception $ex) {\n            $this->storeProjectDetailsLocally($result);\n        }\n\n        // Save authentication token\n        ComposerManager::instance()->addAuthCredentials(\n            $this->getComposerUrl(false),\n            $result['email'],\n            $result['project_id']\n        );\n    }\n\n    /**\n     * storeProjectDetailsLocally instead\n     */\n    protected function storeProjectDetailsLocally($result)\n    {\n        if (!is_dir($cmsStorePath = storage_path('cms'))) {\n            mkdir($cmsStorePath);\n        }\n\n        $this->injectJsonToFile(storage_path('cms/project.json'), [\n            'project' => $result['project_id']\n        ]);\n    }\n\n    /**\n     * requestServerData contacts the update server for a response.\n     * @param  string $uri      Gateway API URI\n     * @param  array  $postData Extra post data\n     * @return array\n     */\n    public function requestServerData(string $uri, array $postData = [])\n    {\n        $result = $this->makeHttpRequest($this->createServerUrl($uri), $postData);\n\n        $contents = $result->body();\n\n        if ($result->status() === 404) {\n            throw new Exception(__('Response Not Found'));\n        }\n\n        if ($result->status() !== 200) {\n            throw new Exception(\n                strlen($contents)\n                ? $contents\n                : __(\"Response Empty\")\n            );\n        }\n\n        $resultData = false;\n\n        try {\n            $resultData = @json_decode($contents, true);\n        }\n        catch (Exception $ex) {\n            throw new Exception(__(\"Response Invalid\"));\n        }\n\n        if ($resultData === false || (is_string($resultData) && !strlen($resultData))) {\n            throw new Exception(__(\"Response Bad Format\"));\n        }\n\n        return $resultData;\n    }\n\n    /**\n     * createServerUrl creates a complete gateway server URL from supplied URI\n     * @param  string $uri URI\n     * @return string      URL\n     */\n    protected function createServerUrl($uri)\n    {\n        $gateway = Config::get('system.update_gateway', 'https://gateway.octobercms.com/api');\n        if (substr($gateway, -1) != '/') {\n            $gateway .= '/';\n        }\n\n        return $gateway . $uri;\n    }\n\n    /**\n     * makeHttpRequest makes a specialized server request to a URL.\n     * @param string $url\n     * @param array $postData\n     * @return \\Illuminate\\Http\\Client\\Response\n     */\n    protected function makeHttpRequest($url, $postData)\n    {\n        // New HTTP instance\n        $http = Http::asForm();\n\n        // Post data\n        $postData['protocol_version'] = '2.0';\n        $postData['client'] = 'October CMS';\n        $postData['server'] = base64_encode(json_encode([\n            'php' => PHP_VERSION,\n            'url' => Url::to('/'),\n            'since' => date('c')\n        ]));\n\n        // Gateway auth\n        if ($credentials = Config::get('system.update_gateway_auth')) {\n            if (is_string($credentials)) {\n                $credentials = explode(':', $credentials);\n            }\n\n            list($user, $pass) = $credentials;\n            $http->withBasicAuth($user, $pass);\n        }\n\n        return $http->post($url, $postData);\n    }\n\n    /**\n     * getComposerUrl returns the endpoint for composer\n     */\n    protected function getComposerUrl(bool $withProtocol = true): string\n    {\n        $gateway = Config::get('system.composer_gateway', 'gateway.octobercms.com');\n\n        return $withProtocol ? 'https://'.$gateway : $gateway;\n    }\n\n    /**\n     * injectJsonToFile merges a JSON array in to an existing JSON file.\n     * Merging is useful for preserving array values.\n     */\n    protected function injectJsonToFile(string $filename, array $jsonArr, bool $merge = false): void\n    {\n        $contentsArr = file_exists($filename)\n            ? json_decode(file_get_contents($filename), true)\n            : [];\n\n        $newArr = $merge\n            ? array_merge_recursive($contentsArr, $jsonArr)\n            : $this->mergeRecursive($contentsArr, $jsonArr);\n\n        $content = json_encode($newArr, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT);\n\n        file_put_contents($filename, $content);\n    }\n\n    /**\n     * mergeRecursive substitutes the native PHP array_merge_recursive to be\n     * more config friendly. Scalar values are replaced instead of being\n     * merged in to their own new array.\n     */\n    protected function mergeRecursive(array $array1, $array2)\n    {\n        if ($array2 && is_array($array2)) {\n            foreach ($array2 as $key => $val2) {\n                if (\n                    is_array($val2) &&\n                    (($val1 = isset($array1[$key]) ? $array1[$key] : null) !== null) &&\n                    is_array($val1)\n                ) {\n                    $array1[$key] = $this->mergeRecursive($val1, $val2);\n                }\n                else {\n                    $array1[$key] = $val2;\n                }\n            }\n        }\n\n        return $array1;\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Console/RouteCacheCommand.php",
    "content": "<?php namespace October\\Rain\\Foundation\\Console;\n\nuse Illuminate\\Foundation\\Console\\RouteCacheCommand as RouteCacheCommandBase;\n\nclass RouteCacheCommand extends RouteCacheCommandBase\n{\n    /**\n     * Boot a fresh copy of the application and get the routes.\n     *\n     * @return \\Illuminate\\Routing\\RouteCollection\n     */\n    protected function getFreshApplicationRoutes()\n    {\n        $routes = $this->getFreshApplication()['router']->registerLateRoutes();\n\n        return tap($routes->getRoutes(), function ($routes) {\n            $routes->refreshNameLookups();\n            $routes->refreshActionLookups();\n        });\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Console/RouteListCommand.php",
    "content": "<?php namespace October\\Rain\\Foundation\\Console;\n\nuse Illuminate\\Routing\\Router;\nuse Illuminate\\Foundation\\Console\\RouteListCommand as RouteListCommandBase;\n\n/**\n * RouteListCommand\n */\nclass RouteListCommand extends RouteListCommandBase\n{\n    /**\n     * __construct a new route command instance.\n     *\n     * @param  \\Illuminate\\Routing\\Router  $router\n     * @return void\n     */\n    public function __construct(Router $router)\n    {\n        if ($router instanceof \\October\\Rain\\Router\\CoreRouter) {\n            $router->registerLateRoutes();\n        }\n\n        parent::__construct($router);\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Console/ServeCommand.php",
    "content": "<?php namespace October\\Rain\\Foundation\\Console;\n\nuse Illuminate\\Foundation\\Console\\ServeCommand as ServeCommandParent;\nuse Symfony\\Component\\Process\\PhpExecutableFinder;\n\nclass ServeCommand extends ServeCommandParent\n{\n    /**\n     * handle the console command.\n     * @return int\n     * @throws \\Exception\n     */\n    public function handle()\n    {\n        if (file_exists(base_path('public'))) {\n            chdir(base_path('public'));\n        }\n\n        $this->line(\"<info>October CMS development server started:</info> http://{$this->host()}:{$this->port()}\");\n\n        $environmentFile = $this->option('env')\n            ? base_path('.env').'.'.$this->option('env')\n            : base_path('.env');\n\n        $hasEnvironment = file_exists($environmentFile);\n\n        $environmentLastModified = $hasEnvironment\n            ? filemtime($environmentFile)\n            : now()->addDays(30)->getTimestamp();\n\n        $process = $this->startProcess($hasEnvironment);\n\n        while ($process->isRunning()) {\n            if ($hasEnvironment) {\n                clearstatcache(false, $environmentFile);\n            }\n\n            if (! $this->option('no-reload') &&\n                $hasEnvironment &&\n                filemtime($environmentFile) > $environmentLastModified) {\n                $environmentLastModified = filemtime($environmentFile);\n\n                $this->comment('Environment modified. Restarting server...');\n\n                $process->stop(5);\n\n                $process = $this->startProcess($hasEnvironment);\n            }\n\n            usleep(500 * 1000);\n        }\n\n        $status = $process->getExitCode();\n\n        if ($status && $this->canTryAnotherPort()) {\n            $this->portOffset += 1;\n\n            return $this->handle();\n        }\n\n        return $status;\n    }\n\n    /**\n     * serverCommand gets the full server command.\n     * @return array\n     */\n    protected function serverCommand()\n    {\n        $server = file_exists(base_path('server.php'))\n            ? base_path('server.php')\n            : __DIR__.'/../resources/server.php';\n\n        return [\n            (new PhpExecutableFinder)->find(false),\n            '-S',\n            $this->host().':'.$this->port(),\n            $server,\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Exception/Handler.php",
    "content": "<?php namespace October\\Rain\\Foundation\\Exception;\n\nuse Event;\nuse Response;\nuse Illuminate\\Auth\\Access\\AuthorizationException;\nuse Illuminate\\Auth\\AuthenticationException;\nuse Illuminate\\Database\\RecordNotFoundException;\nuse Illuminate\\Database\\RecordsNotFoundException;\nuse Illuminate\\Database\\Eloquent\\ModelNotFoundException;\nuse Illuminate\\Http\\Exceptions\\HttpResponseException;\nuse Illuminate\\Http\\Response as HttpResponse;\nuse Illuminate\\Foundation\\Exceptions\\Handler as ExceptionHandler;\nuse Illuminate\\Contracts\\Support\\Responsable;\nuse Illuminate\\Routing\\Exceptions\\BackedEnumCaseNotFoundException;\nuse Illuminate\\Routing\\Router;\nuse Illuminate\\Session\\TokenMismatchException;\nuse Illuminate\\Validation\\ValidationException;\nuse Symfony\\Component\\HttpFoundation\\Exception\\RequestExceptionInterface;\nuse Symfony\\Component\\HttpKernel\\Exception\\AccessDeniedHttpException;\nuse Symfony\\Component\\HttpKernel\\Exception\\BadRequestHttpException;\nuse Symfony\\Component\\HttpKernel\\Exception\\HttpException;\nuse Symfony\\Component\\HttpKernel\\Exception\\HttpExceptionInterface;\nuse Symfony\\Component\\HttpKernel\\Exception\\NotFoundHttpException;\nuse October\\Rain\\Exception\\ForbiddenException;\nuse October\\Rain\\Exception\\NotFoundException;\nuse October\\Rain\\Exception\\AjaxException;\nuse Throwable;\n\n/**\n * Handler is the core exception handler\n */\nclass Handler extends ExceptionHandler\n{\n    /**\n     * @var array dontReport these exception types.\n     */\n    protected $dontReport = [\n        \\October\\Rain\\Exception\\AjaxException::class,\n        \\October\\Rain\\Exception\\NotFoundException::class,\n        \\October\\Rain\\Exception\\ForbiddenException::class,\n        \\October\\Rain\\Exception\\ValidationException::class,\n        \\October\\Rain\\Exception\\ApplicationException::class,\n    ];\n\n    /**\n     * @var array handlers for registered exceptions.\n     */\n    protected $handlers = [];\n\n    /**\n     * report or log an exception.\n     *\n     * This is a great spot to send exceptions to Sentry, Bugsnag, etc.\n     *\n     * @param  \\Throwable  $exception\n     * @return void\n     */\n    public function report(Throwable $exception)\n    {\n        if (!$this->hasBootedEvents()) {\n            return;\n        }\n\n        /**\n         * @event exception.beforeReport\n         * Fires before the exception has been reported\n         *\n         * Example usage (prevents the reporting of a given exception)\n         *\n         *     Event::listen('exception.beforeReport', function (\\Exception $exception) {\n         *         if ($exception instanceof \\My\\Custom\\Exception) {\n         *             return false;\n         *         }\n         *     });\n         */\n        if (Event::fire('exception.beforeReport', [$exception], true) === false) {\n            return;\n        }\n\n        parent::report($exception);\n\n        /**\n         * @event exception.report\n         * Fired after the exception has been reported\n         *\n         * Example usage (performs additional reporting on the exception)\n         *\n         *     Event::listen('exception.report', function (\\Exception $exception) {\n         *         App::make('sentry')->captureException($exception);\n         *     });\n         */\n        Event::fire('exception.report', [$exception]);\n    }\n\n    /**\n     * render an exception into an HTTP response.\n     *\n     * @param  \\Illuminate\\Http\\Request  $request\n     * @param  \\Throwable  $exception\n     * @return \\Symfony\\Component\\HttpFoundation\\Response\n     */\n    public function render($request, Throwable $exception)\n    {\n        // Exception occurred before system has booted\n        if (!$this->hasBootedEvents()) {\n            return parent::render($request, $exception);\n        }\n\n        $exception = $this->mapException($exception);\n\n        // Exception has a render method (Laravel 12)\n        if (method_exists($exception, 'render') && $response = $exception->render($request)) {\n            return $this->finalizeRenderedResponse(\n                $request,\n                Router::toResponse($request, $response),\n                $exception\n            );\n        }\n\n        // Exception wants to return its own response\n        if ($exception instanceof Responsable) {\n            return $this->finalizeRenderedResponse($request, $exception->toResponse($request), $exception);\n        }\n\n        // Convert to public-friendly exception\n        $exception = $this->prepareException($exception);\n\n        // Custom handlers\n        if ($response = $this->renderViaCallbacks($request, $exception)) {\n            return $this->finalizeRenderedResponse($request, $response, $exception);\n        }\n\n        // Exception is a response\n        if ($exception instanceof HttpResponseException) {\n            return $this->finalizeRenderedResponse($request, $exception->getResponse(), $exception);\n        }\n\n        /**\n         * @event exception.beforeRender\n         * Fires as the exception renders and returns an optional custom response.\n         *\n         * Example usage\n         *\n         *     Event::listen('exception.beforeRender', function (\\Exception $exception) {\n         *         return 'An error happened!';\n         *     });\n         */\n        $statusCode = $this->getStatusCode($exception);\n        if (($event = Event::fire('exception.beforeRender', [$exception, $statusCode, $request], true)) !== null) {\n            return $this->finalizeRenderedResponse(\n                $request,\n                Response::make($event, $statusCode),\n                $exception\n            );\n        }\n\n        // Standard Laravel 12 rendering\n        return $this->finalizeRenderedResponse($request, match (true) {\n            $exception instanceof AuthenticationException => $this->unauthenticated($request, $exception),\n            $exception instanceof ValidationException => $this->convertValidationExceptionToResponse($exception, $request),\n            default => $this->renderExceptionResponse($request, $exception),\n        }, $exception);\n    }\n\n    /**\n     * prepareException for rendering.\n     *\n     * @param  \\Throwable  $e\n     * @return \\Throwable\n     */\n    protected function prepareException(Throwable $e): Throwable\n    {\n        return match (true) {\n            // October-specific: NotFoundException → NotFoundHttpException\n            $e instanceof NotFoundException => new NotFoundHttpException($e->getMessage(), $e),\n\n            // Laravel 12 standard conversions\n            $e instanceof BackedEnumCaseNotFoundException => new NotFoundHttpException($e->getMessage(), $e),\n            $e instanceof ModelNotFoundException => new NotFoundHttpException($e->getMessage(), $e),\n            $e instanceof AuthorizationException && $e->hasStatus() => new HttpException(\n                $e->status(), $e->response()?->message() ?: (HttpResponse::$statusTexts[$e->status()] ?? 'Whoops, looks like something went wrong.'), $e\n            ),\n            $e instanceof AuthorizationException && ! $e->hasStatus() => new AccessDeniedHttpException($e->getMessage(), $e),\n            $e instanceof TokenMismatchException => new HttpException(419, $e->getMessage(), $e),\n            $e instanceof RequestExceptionInterface => new BadRequestHttpException('Bad request.', $e),\n            $e instanceof RecordNotFoundException => new NotFoundHttpException('Not found.', $e),\n            $e instanceof RecordsNotFoundException => new NotFoundHttpException('Not found.', $e),\n            default => $e,\n        };\n    }\n\n    /**\n     * getStatusCode checks if the exception implements the HttpExceptionInterface, or returns\n     * as generic 500 error code for a server side error.\n     * @param \\Exception $exception\n     * @return int\n     */\n    protected function getStatusCode($exception)\n    {\n        if ($exception instanceof HttpExceptionInterface) {\n            $code = $exception->getStatusCode();\n        }\n        elseif ($exception instanceof ForbiddenException) {\n            $code = 403;\n        }\n        elseif ($exception instanceof NotFoundHttpException) {\n            $code = 404;\n        }\n        elseif ($exception instanceof AjaxException) {\n            $code = 406;\n        }\n        else {\n            $code = 500;\n        }\n\n        return $code;\n    }\n\n    /**\n     * context is the the default context variables for logging.\n     *\n     * @return array\n     */\n    protected function context()\n    {\n        return [];\n    }\n\n    //\n    // Custom handlers\n    //\n\n    /**\n     * @deprecated use renderable\n     */\n    public function error(callable $callback)\n    {\n        $this->renderable($callback);\n    }\n\n    /**\n     * hasBootedEvents checks if we can broadcast events\n     */\n    protected function hasBootedEvents(): bool\n    {\n        if (!class_exists('Event')) {\n            return false;\n        }\n\n        if (!$app = Event::getFacadeApplication()) {\n            return false;\n        }\n\n        if (!$app->bound('events')) {\n            return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Http/Kernel.php",
    "content": "<?php namespace October\\Rain\\Foundation\\Http;\n\nuse Illuminate\\Foundation\\Http\\Kernel as HttpKernel;\n\n/**\n * Kernel\n *\n * @package october\\foundation\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Kernel extends HttpKernel\n{\n    /**\n     * The bootstrap classes for the application.\n     *\n     * @var array\n     */\n    protected $bootstrappers = [\n        \\Illuminate\\Foundation\\Bootstrap\\LoadEnvironmentVariables::class,\n        \\October\\Rain\\Foundation\\Bootstrap\\LoadConfiguration::class,\n        \\October\\Rain\\Foundation\\Bootstrap\\HandleExceptions::class,\n        \\Illuminate\\Foundation\\Bootstrap\\RegisterFacades::class,\n        \\October\\Rain\\Foundation\\Bootstrap\\RegisterOctober::class,\n        \\Illuminate\\Foundation\\Bootstrap\\RegisterProviders::class,\n        \\Illuminate\\Foundation\\Bootstrap\\BootProviders::class,\n    ];\n\n    /**\n     * @var array middleware is the application's global HTTP middleware stack.\n     */\n    protected $middleware = [];\n\n    /**\n     * @var array routeMiddleware is the application's route middleware.\n     */\n    protected $routeMiddleware = [];\n\n    /**\n     * @var array middlewareGroups is the application's route middleware groups.\n     */\n    protected $middlewareGroups = [];\n\n    /**\n     * @var array middlewarePriority is the priority-sorted list of middleware.\n     *\n     * Forces the listed middleware to always be in the given order.\n     */\n    protected $middlewarePriority = [\n        \\Illuminate\\Foundation\\Http\\Middleware\\HandlePrecognitiveRequests::class,\n        \\October\\Rain\\Foundation\\Http\\Middleware\\EncryptCookies::class,\n        \\Illuminate\\Cookie\\Middleware\\EncryptCookies::class,\n        \\Illuminate\\Cookie\\Middleware\\AddQueuedCookiesToResponse::class,\n        \\Illuminate\\Session\\Middleware\\StartSession::class,\n        \\Illuminate\\View\\Middleware\\ShareErrorsFromSession::class,\n        \\Illuminate\\Contracts\\Auth\\Middleware\\AuthenticatesRequests::class,\n        \\Illuminate\\Routing\\Middleware\\ThrottleRequests::class,\n        \\Illuminate\\Routing\\Middleware\\ThrottleRequestsWithRedis::class,\n        \\Illuminate\\Contracts\\Session\\Middleware\\AuthenticatesSessions::class,\n        \\Illuminate\\Routing\\Middleware\\SubstituteBindings::class,\n        \\Illuminate\\Auth\\Middleware\\Authorize::class,\n    ];\n}\n"
  },
  {
    "path": "src/Foundation/Http/Middleware/CheckForMaintenanceMode.php",
    "content": "<?php namespace October\\Rain\\Foundation\\Http\\Middleware;\n\nuse View;\nuse Closure;\nuse Response;\nuse Illuminate\\Foundation\\Http\\Middleware\\PreventRequestsDuringMaintenance;\nuse Symfony\\Component\\HttpKernel\\Exception\\HttpException;\n\n/**\n * CheckForMaintenanceMode\n *\n * @package october\\foundation\n * @author Alexey Bobkov, Samuel Georges\n */\nclass CheckForMaintenanceMode extends PreventRequestsDuringMaintenance\n{\n    /**\n     * handle an incoming request.\n     */\n    public function handle($request, Closure $next)\n    {\n        try {\n            return parent::handle($request, $next);\n        }\n        catch (HttpException $ex) {\n            $view = View::exists('app::maintenance') ? 'app::maintenance' : 'system::maintenance';\n            $data = $this->app->maintenanceMode()->data();\n\n            return Response::make(\n                View::make($view, [\n                    'message' => $ex->getMessage(),\n                    'retryAfter' => $data['retry'] ?? null,\n                ]),\n                $ex->getStatusCode(),\n                $ex->getHeaders()\n            );\n        }\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Http/Middleware/EncryptCookies.php",
    "content": "<?php namespace October\\Rain\\Foundation\\Http\\Middleware;\n\nuse Config;\nuse Illuminate\\Contracts\\Encryption\\Encrypter as EncrypterContract;\nuse Illuminate\\Cookie\\Middleware\\EncryptCookies as EncryptCookiesBase;\n\n/**\n * EncryptCookies\n *\n * @package october\\foundation\n * @author Alexey Bobkov, Samuel Georges\n */\nclass EncryptCookies extends EncryptCookiesBase\n{\n    /**\n     * __construct\n     */\n    public function __construct(EncrypterContract $encrypter)\n    {\n        parent::__construct($encrypter);\n\n        // Cookies that should not be encrypted\n        $except = is_array($labels = Config::get('system.unencrypt_cookies'))\n            ? $labels\n            : (array) json_decode($labels, true);\n\n        $this->disableFor($except);\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Providers/AppServiceProvider.php",
    "content": "<?php\n\nnamespace October\\Rain\\Foundation\\Providers;\n\nuse October\\Rain\\Support\\ServiceProvider;\nuse Illuminate\\Contracts\\Support\\DeferrableProvider;\n\n/**\n * AppServiceProvider contains providers for running October CMS\n *\n * @package october\\foundation\n * @author Alexey Bobkov, Samuel Georges\n */\nclass AppServiceProvider extends ServiceProvider implements DeferrableProvider\n{\n    /**\n     * register the service provider.\n     */\n    public function register()\n    {\n        $this->app->singleton('october.installer', \\October\\Rain\\Installer\\InstallManager::class);\n        $this->registerConsoleCommand('october.build', \\October\\Rain\\Installer\\Console\\OctoberBuild::class);\n        $this->registerConsoleCommand('october.install', \\October\\Rain\\Installer\\Console\\OctoberInstall::class);\n    }\n\n    /**\n     * registerConsoleCommand registers a new console (artisan) command\n     */\n    protected function registerConsoleCommand(string $key, string $class)\n    {\n        $key = 'command.'.$key;\n\n        $this->app->singleton($key, function ($app) use ($class) {\n            return $this->app->make($class);\n        });\n\n        $this->commands($key);\n    }\n\n    /**\n     * provides the returned services.\n     * @return array\n     */\n    public function provides()\n    {\n        return [\n            'october.installer',\n            'command.october.build',\n            'command.october.install',\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Providers/ArtisanServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Foundation\\Providers;\n\nuse Illuminate\\Console\\Signals;\nuse October\\Rain\\Foundation\\Console\\ServeCommand;\nuse October\\Rain\\Foundation\\Console\\RouteListCommand;\nuse October\\Rain\\Foundation\\Console\\RouteCacheCommand;\nuse October\\Rain\\Foundation\\Console\\ProjectSetCommand;\nuse October\\Rain\\Foundation\\Console\\ClearCompiledCommand;\nuse Illuminate\\Foundation\\Providers\\ArtisanServiceProvider as ArtisanServiceProviderBase;\n\n/**\n * ArtisanServiceProvider\n */\nclass ArtisanServiceProvider extends ArtisanServiceProviderBase\n{\n    /**\n     * The commands to be registered.\n     *\n     * @var array\n     */\n    protected $commandsRain = [\n        'RouteList' => RouteListCommand::class,\n        'RouteCache' => RouteCacheCommand::class,\n        'ProjectSet' => ProjectSetCommand::class,\n        'ClearCompiled' => ClearCompiledCommand::class,\n    ];\n\n    /**\n     * @var array devCommands to be registered.\n     */\n    protected $devCommandsRain = [\n        'Serve' => ServeCommand::class,\n    ];\n\n    /**\n     * register the service provider\n     */\n    public function register()\n    {\n        $this->registerCommands(array_merge(\n            $this->commands,\n            $this->commandsRain,\n            $this->devCommands,\n            $this->devCommandsRain\n        ));\n\n        Signals::resolveAvailabilityUsing(function () {\n            return $this->app->runningInConsole()\n                && ! $this->app->runningUnitTests()\n                && extension_loaded('pcntl');\n        });\n    }\n\n    /**\n     * registerRouteCacheCommand\n     */\n    protected function registerRouteCacheCommand()\n    {\n        $this->app->singleton(RouteCacheCommand::class, function ($app) {\n            return new RouteCacheCommand($app['files']);\n        });\n    }\n\n    /**\n     * registerRouteListCommand\n     */\n    protected function registerRouteListCommand()\n    {\n        $this->app->singleton(RouteListCommand::class, function ($app) {\n            return new RouteListCommand($app['router']);\n        });\n    }\n\n    /**\n     * registerServeCommand\n     */\n    protected function registerServeCommand()\n    {\n        $this->app->singleton(ServeCommand::class, function () {\n            return new ServeCommand;\n        });\n    }\n\n    /**\n     * registerClearCompiledCommand\n     */\n    protected function registerClearCompiledCommand()\n    {\n        $this->app->singleton(ClearCompiledCommand::class, function () {\n            return new ClearCompiledCommand;\n        });\n    }\n\n    /**\n     * registerProjectSetCommand\n     */\n    protected function registerProjectSetCommand()\n    {\n        $this->app->singleton(ProjectSetCommand::class, function () {\n            return new ProjectSetCommand;\n        });\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Providers/ConsoleSupportServiceProvider.php",
    "content": "<?php\n\nnamespace October\\Rain\\Foundation\\Providers;\n\nuse Illuminate\\Foundation\\Providers\\ConsoleSupportServiceProvider as ConsoleSupportServiceProviderBase;\n\nclass ConsoleSupportServiceProvider extends ConsoleSupportServiceProviderBase\n{\n    /**\n     * The provider class names.\n     *\n     * @var string[]\n     */\n    protected $providers = [\n        \\October\\Rain\\Foundation\\Providers\\ArtisanServiceProvider::class,\n        \\Illuminate\\Database\\MigrationServiceProvider::class,\n        \\Illuminate\\Foundation\\Providers\\ComposerServiceProvider::class,\n    ];\n}\n"
  },
  {
    "path": "src/Foundation/Providers/CoreServiceProvider.php",
    "content": "<?php\n\nnamespace October\\Rain\\Foundation\\Providers;\n\nuse October\\Rain\\Support\\ServiceProvider;\nuse Illuminate\\Contracts\\Support\\DeferrableProvider;\n\n/**\n * CoreServiceProvider contains providers for running October Rain\n *\n * @package october\\foundation\n * @author Alexey Bobkov, Samuel Georges\n */\nclass CoreServiceProvider extends ServiceProvider implements DeferrableProvider\n{\n    /**\n     * register the service provider.\n     */\n    public function register()\n    {\n        $this->app->singleton('core.composer', \\October\\Rain\\Composer\\ComposerManager::class);\n    }\n\n    /**\n     * provides the returned services.\n     * @return array\n     */\n    public function provides()\n    {\n        return [\n            'core.composer',\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Providers/DateServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Foundation\\Providers;\n\nuse Carbon\\Carbon;\nuse Carbon\\CarbonImmutable;\nuse Carbon\\CarbonInterval;\nuse Carbon\\CarbonPeriod;\nuse Illuminate\\Support\\DateFactory;\nuse October\\Rain\\Support\\ServiceProvider;\n\n/**\n * DateServiceProvider\n */\nclass DateServiceProvider extends ServiceProvider\n{\n    /**\n     * register the service provider.\n     */\n    public function register()\n    {\n        DateFactory::useClass(\\October\\Rain\\Support\\Date::class);\n    }\n\n    /**\n     * boot the application events\n     */\n    public function boot()\n    {\n        $locale = $this->app['config']->get('app.locale');\n\n        $this->setCarbonLocale($locale);\n\n        $this->app['events']->listen('locale.changed', function ($locale) {\n            $this->setCarbonLocale($locale);\n        });\n    }\n\n    /**\n     * setCarbonLocale sets the locale using the correct load order.\n     */\n    protected function setCarbonLocale($locale)\n    {\n        Carbon::setLocale($locale);\n        CarbonImmutable::setLocale($locale);\n        CarbonPeriod::setLocale($locale);\n        CarbonInterval::setLocale($locale);\n\n        $fallbackLocale = $this->getFallbackLocale($locale);\n        if ($locale !== $fallbackLocale) {\n            Carbon::setFallbackLocale($fallbackLocale);\n        }\n    }\n\n    /**\n     * Split the locale and use it as the fallback.\n     */\n    protected function getFallbackLocale($locale)\n    {\n        if ($position = strpos($locale, '-')) {\n            $target = substr($locale, 0, $position);\n            $resource = __DIR__ . '/../../../../nesbot/carbon/src/Carbon/Lang/'.$target.'.php';\n            if (file_exists($resource)) {\n                return $target;\n            }\n        }\n\n        return $this->app['config']->get('app.fallback_locale');\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Providers/ExecutionContextProvider.php",
    "content": "<?php namespace October\\Rain\\Foundation\\Providers;\n\nuse Illuminate\\Support\\ServiceProvider;\n\n/**\n * ExecutionContextProvider sets the execution context globally\n */\nclass ExecutionContextProvider extends ServiceProvider\n{\n    /**\n     * register the service provider.\n     */\n    public function register()\n    {\n        $this->app->scoped('execution.context', function ($app) {\n            return $this->determineContext($app);\n        });\n    }\n\n    /**\n     * boot the service provider.\n     */\n    public function boot()\n    {\n        // Refresh execution context when Octane receives a new request\n        if (class_exists(\\Laravel\\Octane\\Events\\RequestReceived::class)) {\n            $this->app['events']->listen(\\Laravel\\Octane\\Events\\RequestReceived::class, function ($event) {\n                $event->sandbox->forgetInstance('execution.context');\n            });\n        }\n    }\n\n    /**\n     * determineContext evaluates the execution context from the current request.\n     */\n    protected function determineContext($app): string\n    {\n        $requestPath = $this->normalizeUrl($app['request']->path());\n\n        $backendUri = $this->normalizeUrl($app['config']->get('backend.uri', 'backend'));\n\n        if (str_starts_with($requestPath, $backendUri)) {\n            return 'backend';\n        }\n\n        return 'frontend';\n    }\n\n    /**\n     * normalizeUrl adds leading slash from a URL.\n     *\n     * @param string $url URL to normalize.\n     * @return string Returns normalized URL.\n     */\n    protected function normalizeUrl($url)\n    {\n        if (substr($url, 0, 1) !== '/') {\n            $url = '/'.$url;\n        }\n\n        if (!strlen($url)) {\n            $url = '/';\n        }\n\n        return $url;\n    }\n}\n"
  },
  {
    "path": "src/Foundation/Providers/LogServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Foundation\\Providers;\n\nuse Illuminate\\Log\\LogServiceProvider as LogServiceProviderBase;\n\n/**\n * LogServiceProvider\n */\nclass LogServiceProvider extends LogServiceProviderBase\n{\n    /**\n     * register the service provider.\n     */\n    public function register()\n    {\n        parent::register();\n\n        // After registration\n        $this->app->booting(function () {\n            $this->configureDefaultLogger($this->app['config']);\n            $this->configureDefaultPermissions($this->app['config'], $this->app['files']);\n        });\n    }\n\n    /**\n     * configureDefaultLogger channel for the application\n     * when no configuration is supplied.\n     */\n    protected function configureDefaultLogger($config)\n    {\n        if ($config->get('logging.default', null) !== null) {\n            return;\n        }\n\n        // Set default values as single log file\n        $config->set('logging.default', 'single');\n\n        $config->set('logging.channels.single', [\n            'driver' => 'single',\n            'path' => storage_path('logs/system.log'),\n            'level' => 'debug',\n        ]);\n    }\n\n    /**\n     * configureDefaultPermissions\n     */\n    protected function configureDefaultPermissions($config, $files)\n    {\n        if ($config->get('logging.channels.single.permission', null) === null) {\n            $config->set('logging.channels.single.permission', $files->getFilePermissions());\n        }\n\n        if ($config->get('logging.channels.daily.permission', null) === null) {\n            $config->set('logging.channels.daily.permission', $files->getFilePermissions());\n        }\n    }\n}\n"
  },
  {
    "path": "src/Foundation/resources/server.php",
    "content": "<?php\n\n$publicPath = getcwd();\n\n$uri = urldecode(\n    parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?? ''\n);\n\n// This file allows us to emulate Apache's \"mod_rewrite\" functionality from the\n// built-in PHP web server. This provides a convenient way to test an October CMS\n// application without having installed a \"real\" web server software here.\nif ($uri !== '/' && file_exists($publicPath.$uri)) {\n    return false;\n}\n\nrequire_once file_exists($publicPath . '/index.php')\n    ? $publicPath . '/index.php'\n    : $publicPath . '/public/index.php';\n"
  },
  {
    "path": "src/Halcyon/Builder.php",
    "content": "<?php namespace October\\Rain\\Halcyon;\n\nuse Carbon\\Carbon;\nuse Illuminate\\Support\\Arr;\nuse October\\Rain\\Halcyon\\Exception\\InvalidDirectoryNameException;\nuse October\\Rain\\Halcyon\\Exception\\InvalidExtensionException;\nuse October\\Rain\\Halcyon\\Exception\\MissingFileNameException;\nuse October\\Rain\\Halcyon\\Exception\\InvalidFileNameException;\nuse October\\Rain\\Halcyon\\Datasource\\DatasourceInterface;\nuse October\\Rain\\Halcyon\\Processors\\Processor;\nuse BadMethodCallException;\nuse ApplicationException;\n\n/**\n * Builder for Halcyon queries\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Builder\n{\n    /**\n     * @var \\October\\Rain\\Halcyon\\Datasource\\DatasourceInterface datasource instance\n     */\n    protected $datasource;\n\n    /**\n     * @var \\October\\Rain\\Halcyon\\Model model being queried\n     */\n    protected $model;\n\n    /**\n     * @var \\October\\Rain\\Halcyon\\Processors\\Processor processor datasource query post processor instance\n     */\n    protected $processor;\n\n    /**\n     * @var array columns that should be returned\n     */\n    public $columns;\n\n    /**\n     * @var array extensions filter the query by these file extensions\n     */\n    public $extensions;\n\n    /**\n     * @var string from the directory name which the query is targeting\n     */\n    public $from;\n\n    /**\n     * @var bool selectSingle query should pluck a single record\n     */\n    public $selectSingle;\n\n    /**\n     * @var string fileMatch using the specified pattern\n     */\n    public $fileMatch;\n\n    /**\n     * @var array orders is the orderings for the query\n     */\n    public $orders;\n\n    /**\n     * @var int limit is the maximum number of records to return\n     */\n    public $limit;\n\n    /**\n     * @var int offset is the number of records to skip\n     */\n    public $offset;\n\n    /**\n     * @var string cacheKey that should be used when caching the query\n     */\n    protected $cacheKey;\n\n    /**\n     * @var int cacheMinutes number of minutes to cache the query\n     */\n    protected $cacheMinutes;\n\n    /**\n     * @var array cacheTags for the query cache\n     */\n    protected $cacheTags;\n\n    /**\n     * @var string cacheDriver to be used\n     */\n    protected $cacheDriver;\n\n    /**\n     * @var bool loadedFromCache is an internal variable to specify if the record was loaded from cache\n     */\n    protected $loadedFromCache = false;\n\n    /**\n     * __construct creates a new query builder instance\n     */\n    public function __construct(DatasourceInterface $datasource, Processor $processor)\n    {\n        $this->datasource = $datasource;\n        $this->processor = $processor;\n    }\n\n    /**\n     * whereFileName switches mode to select a single template by its name\n     * @param  string  $fileName\n     * @return $this\n     */\n    public function whereFileName($fileName)\n    {\n        $this->selectSingle = $this->model->getFileNameParts($fileName);\n\n        return $this;\n    }\n\n    /**\n     * from sets the directory name which the query is targeting\n     * @param  string  $dirName\n     * @return $this\n     */\n    public function from($dirName)\n    {\n        $this->from = $dirName;\n\n        return $this;\n    }\n\n    /**\n     * offset sets the \"offset\" value of the query\n     * @param  int  $value\n     * @return $this\n     */\n    public function offset($value)\n    {\n        $this->offset = max(0, $value);\n\n        return $this;\n    }\n\n    /**\n     * skip is an alias to set the \"offset\" value of the query\n     * @param  int  $value\n     * @return \\October\\Rain\\Halcyon\\Builder|static\n     */\n    public function skip($value)\n    {\n        return $this->offset($value);\n    }\n\n    /**\n     * limit sets the \"limit\" value of the query\n     * @param  int  $value\n     * @return $this\n     */\n    public function limit($value)\n    {\n        if ($value >= 0) {\n            $this->limit = $value;\n        }\n\n        return $this;\n    }\n\n    /**\n     * take is an alias to set the \"limit\" value of the query\n     * @param  int  $value\n     * @return \\October\\Rain\\Halcyon\\Builder|static\n     */\n    public function take($value)\n    {\n        return $this->limit($value);\n    }\n\n    /**\n     * find a single template by its file name.\n     * @param  string $fileName\n     * @return mixed|static\n     */\n    public function find($fileName)\n    {\n        return $this->whereFileName($fileName)->first();\n    }\n\n    /**\n     * first executes the query and get the first result\n     * @return mixed|static\n     */\n    public function first()\n    {\n        return $this->limit(1)->get()->first();\n    }\n\n    /**\n     * get executes the query as a \"select\" statement\n     * @param  array  $columns\n     * @return \\October\\Rain\\Halcyon\\Collection|static[]\n     */\n    public function get($columns = ['*'])\n    {\n        if (!is_null($this->cacheMinutes)) {\n            $results = $this->getCached($columns);\n        }\n        else {\n            $results = $this->getFresh($columns);\n        }\n\n        $models = $this->getModels($results ?: []);\n\n        return $this->model->newCollection($models);\n    }\n\n    /**\n     * pluck gets an array with the values of a given column\n     * @param  string  $column\n     * @param  string  $key\n     * @return Collection\n     */\n    public function pluck($column, $key = null)\n    {\n        $select = is_null($key) ? [$column] : [$column, $key];\n\n        if (!is_null($this->cacheMinutes)) {\n            $results = $this->getCached($select);\n        }\n        else {\n            $results = $this->getFresh($select);\n        }\n\n        $collection = new Collection($results);\n\n        return $collection->pluck($column, $key);\n    }\n\n    /**\n     * lists is short-hand for pluck()->all()\n     * @param  string  $column\n     * @param  string  $key\n     * @return array\n     */\n    public function lists($column, $key = null)\n    {\n        return $this->pluck($column, $key)->all();\n    }\n\n    /**\n     * getFresh executes the query as a fresh \"select\" statement\n     * @param  array  $columns\n     * @return \\October\\Rain\\Halcyon\\Collection|static[]\n     */\n    public function getFresh($columns = ['*'])\n    {\n        if (is_null($this->columns)) {\n            $this->columns = $columns;\n        }\n\n        $processCmd = $this->selectSingle ? 'processSelectOne' : 'processSelect';\n\n        return $this->processor->{$processCmd}($this, $this->runSelect());\n    }\n\n    /**\n     * runSelect runs the query as a \"select\" statement against the datasource\n     * @return array\n     */\n    protected function runSelect()\n    {\n        if (!is_string($this->from)) {\n            throw new ApplicationException(sprintf(\"The from property is invalid, make sure that %s has a string value for its \\$dirName property (use '' if not using directories)\", get_class($this->model)));\n        }\n\n        $this->validateDirectoryName($this->from);\n\n        if ($this->selectSingle) {\n            list($name, $extension) = $this->selectSingle;\n\n            $this->validateFileName($name . '.' . $extension);\n\n            return $this->datasource->selectOne($this->from, $name, $extension);\n        }\n\n        return $this->datasource->select($this->from, [\n            'columns' => $this->columns,\n            'extensions' => $this->extensions\n        ]);\n    }\n\n    /**\n     * setModel instance for the model being queried\n     * @return $this\n     */\n    public function setModel(Model $model)\n    {\n        $this->model = $model;\n\n        $this->extensions = $this->model->getAllowedExtensions();\n\n        $this->from($this->model->getObjectTypeDirName());\n\n        return $this;\n    }\n\n    /**\n     * toCompiled gets the compiled file content representation of the query\n     * @return string\n     */\n    public function toCompiled()\n    {\n        return $this->processor->processUpdate($this, []);\n    }\n\n    /**\n     * insert a new record into the datasource.\n     * @return bool\n     */\n    public function insert(array $values)\n    {\n        if (empty($values)) {\n            return true;\n        }\n\n        $this->validateFileName();\n\n        list($name, $extension) = $this->model->getFileNameParts();\n\n        $result = $this->processor->processInsert($this, $values);\n\n        return $this->datasource->insert(\n            $this->model->getObjectTypeDirName(),\n            $name,\n            $extension,\n            $result\n        );\n    }\n\n    /**\n     * update a record in the datasource.\n     * @return int\n     */\n    public function update(array $values)\n    {\n        $this->validateFileName();\n\n        list($name, $extension) = $this->model->getFileNameParts();\n\n        $result = $this->processor->processUpdate($this, $values);\n\n        $oldName = $oldExtension = null;\n\n        if ($this->model->isDirty('fileName')) {\n            list($oldName, $oldExtension) = $this->model->getFileNameParts(\n                $this->model->getOriginal('fileName')\n            );\n        }\n\n        return $this->datasource->update(\n            $this->model->getObjectTypeDirName(),\n            $name,\n            $extension,\n            $result,\n            $oldName,\n            $oldExtension\n        );\n    }\n\n    /**\n     * delete a record from the database.\n     * @param  string  $fileName\n     * @return int\n     */\n    public function delete()\n    {\n        $this->validateFileName();\n\n        list($name, $extension) = $this->model->getFileNameParts();\n\n        return $this->datasource->delete(\n            $this->model->getObjectTypeDirName(),\n            $name,\n            $extension\n        );\n    }\n\n    /**\n     * lastModified returns the last modified time of the object\n     * @return int\n     */\n    public function lastModified()\n    {\n        $this->validateFileName();\n\n        list($name, $extension) = $this->model->getFileNameParts();\n\n        return $this->datasource->lastModified(\n            $this->model->getObjectTypeDirName(),\n            $name,\n            $extension\n        );\n    }\n\n    /**\n     * getModels gets the hydrated models\n     * @param  array  $results\n     * @return \\October\\Rain\\Halcyon\\Model[]\n     */\n    public function getModels(array $results)\n    {\n        $datasource = $this->model->getDatasourceName();\n\n        $models = $this->model->hydrate($results, $datasource);\n\n        /*\n         * Flag the models as loaded from cache, then reset the internal property.\n         */\n        if ($this->loadedFromCache) {\n            $models->each(function ($model) {\n                $model->setLoadedFromCache($this->loadedFromCache);\n            });\n\n            $this->loadedFromCache = false;\n        }\n\n        return $models->all();\n    }\n\n    /**\n     * getModel instance being queried\n     * @return \\October\\Rain\\Halcyon\\Model\n     */\n    public function getModel()\n    {\n        return $this->model;\n    }\n\n    //\n    // Validation (Hard)\n    //\n\n    /**\n     * validateFileName validates the supplied filename, extension and path\n     * @param string $fileName\n     */\n    protected function validateFileName($fileName = null)\n    {\n        if ($fileName === null) {\n            $fileName = $this->model->fileName;\n        }\n\n        if (!strlen($fileName)) {\n            throw (new MissingFileNameException)->setModel($this->model);\n        }\n\n        if (!$this->validateFileNamePath($fileName, $this->model->getMaxNesting())) {\n            throw (new InvalidFileNameException)->setInvalidFileName($fileName);\n        }\n\n        $this->validateFileNameExtension($fileName, $this->model->getAllowedExtensions());\n\n        return true;\n    }\n\n    /**\n     * validateDirectoryName validates the supplied directory path\n     * @param string $dirName\n     */\n    protected function validateDirectoryName($dirName = null)\n    {\n        if (!$this->validateFileNamePath($dirName, $this->model->getMaxNesting())) {\n            throw (new InvalidDirectoryNameException)->setInvalidDirectoryName($dirName);\n        }\n\n        return true;\n    }\n\n    /**\n     * validateFileNameExtension validates whether a file has an allowed extension\n     * @param string $fileName Specifies a path to validate\n     * @param array $allowedExtensions A list of allowed file extensions\n     * @return void\n     */\n    protected function validateFileNameExtension($fileName, $allowedExtensions)\n    {\n        $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));\n\n        if (strlen($extension) && !in_array($extension, $allowedExtensions)) {\n            throw (new InvalidExtensionException)\n                ->setInvalidExtension($extension)\n                ->setAllowedExtensions($allowedExtensions)\n            ;\n        }\n    }\n\n    /**\n     * validateFileNamePath validates a template path\n     * Template directory and file names can contain only alphanumeric symbols, dashes and dots.\n     * @param string $filePath Specifies a path to validate\n     * @param int $maxNesting Specifies the maximum allowed nesting level\n     * @return void\n     */\n    protected function validateFileNamePath($filePath, $maxNesting = 5)\n    {\n        if (strpos($filePath, '..') !== false) {\n            return false;\n        }\n\n        if (strpos($filePath, './') !== false || strpos($filePath, '//') !== false) {\n            return false;\n        }\n\n        $segments = explode('/', $filePath);\n        if ($maxNesting !== null && count($segments) > ($maxNesting + 1)) {\n            return false;\n        }\n\n        foreach ($segments as $segment) {\n            if (!$this->validateFileNamePattern($segment)) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * validateFileNamePattern validates a template file or directory name\n     * template file names can contain only alphanumeric symbols, dashes, underscores and dots.\n     * @param string $fileName Specifies a path to validate\n     * @return boolean Returns true if the file name is valid. Otherwise returns false.\n     */\n    protected function validateFileNamePattern($fileName)\n    {\n        return preg_match('/^[a-z0-9\\_\\-\\.\\/]+$/i', $fileName) ? true : false;\n    }\n\n    //\n    // Caching\n    //\n\n    /**\n     * remember indicates that the query results should be cached\n     * @param  \\DateTime|int  $minutes\n     * @param  string  $key\n     * @return $this\n     */\n    public function remember($minutes, $key = null)\n    {\n        list($this->cacheMinutes, $this->cacheKey) = [$minutes, $key];\n\n        return $this;\n    }\n\n    /**\n     * rememberForever indicates that the query results should be cached forever\n     * @param  string  $key\n     * @return $this\n     */\n    public function rememberForever($key = null)\n    {\n        return $this->remember(-1, $key);\n    }\n\n    /**\n     * cacheTags indicates that the results, if cached, should use the given cache tags\n     * @param  array|mixed  $cacheTags\n     * @return $this\n     */\n    public function cacheTags($cacheTags)\n    {\n        $this->cacheTags = $cacheTags;\n\n        return $this;\n    }\n\n    /**\n     * cacheDriver indicate that the results, if cached, should use the given cache driver\n     * @param  string  $cacheDriver\n     * @return $this\n     */\n    public function cacheDriver($cacheDriver)\n    {\n        $this->cacheDriver = $cacheDriver;\n\n        return $this;\n    }\n\n    /**\n     * getCached executes the query as a cached \"select\" statement\n     * @param  array  $columns\n     * @return array\n     */\n    public function getCached($columns = ['*'])\n    {\n        if (is_null($this->columns)) {\n            $this->columns = $columns;\n        }\n\n        $key = $this->getCacheKey();\n        $minutes = $this->cacheMinutes;\n        $cache = $this->getCache();\n        $callback = $this->getCacheCallback($columns);\n        $result = $cache->get($key);\n        $isNewCache = $result === null;\n\n        // If this is an old cache record, we can check if the cache has been busted\n        // by comparing the modification times. If this is the case, forget the\n        // cache and then prompt a recycle of the results.\n        if (!$isNewCache && $this->isCacheBusted($result)) {\n            $cache->forget($key);\n            $isNewCache = true;\n        }\n\n        // If the \"minutes\" value is less than zero, we will use that as the indicator\n        // that the value should be remembered values should be stored indefinitely\n        // and if we have minutes we will use the typical remember function here.\n        if ($isNewCache) {\n            $result = $callback();\n            if ($minutes < 0) {\n                $cache->forever($key, $result);\n            }\n            else {\n                $expiresAt = Carbon::now()->addMinutes($minutes);\n                $cache->put($key, $result, $expiresAt);\n            }\n        }\n\n        $this->loadedFromCache = !$isNewCache;\n\n        return $result;\n    }\n\n    /**\n     * isCacheBusted returns true if the cache for the file is busted. This only applies\n     * to single record selection\n     * @param  array  $result\n     * @return bool\n     */\n    protected function isCacheBusted($result)\n    {\n        if (!$this->selectSingle) {\n            return false;\n        }\n\n        $mtime = $result ? Arr::get(reset($result), 'mtime') : null;\n\n        list($name, $extension) = $this->selectSingle;\n\n        $currentMtime = $this->datasource->lastModified(\n            $this->from,\n            $name,\n            $extension\n        );\n\n        return $currentMtime !== $mtime;\n    }\n\n    /**\n     * getCache object with tags assigned, if applicable\n     * @return \\Illuminate\\Cache\\CacheManager\n     */\n    protected function getCache()\n    {\n        $cache = $this->model->getCacheManager()->driver($this->cacheDriver);\n\n        return $this->cacheTags ? $cache->tags($this->cacheTags) : $cache;\n    }\n\n    /**\n     * getCacheKey gets a unique cache key for the complete query\n     * @return string\n     */\n    public function getCacheKey()\n    {\n        return $this->cacheKey ?: $this->generateCacheKey();\n    }\n\n    /**\n     * generateCacheKey for the query\n     * @return string\n     */\n    public function generateCacheKey()\n    {\n        $payload = [];\n        $payload[] = $this->selectSingle ? serialize($this->selectSingle) : '*';\n        $payload[] = $this->orders ? serialize($this->orders) : '*';\n        $payload[] = $this->columns ? serialize($this->columns) : '*';\n        $payload[] = $this->fileMatch;\n        $payload[] = $this->limit;\n        $payload[] = $this->offset;\n\n        return 'halcyon_'.$this->from.'_'.$this->datasource->makeCacheKey(implode('-', $payload));\n    }\n\n    /**\n     * getCacheCallback used when caching queries\n     * @param  string  $fileName\n     * @return \\Closure\n     */\n    protected function getCacheCallback($columns)\n    {\n        return function () use ($columns) {\n            return $this->processInitCacheData($this->getFresh($columns));\n        };\n    }\n\n    /**\n     * processInitCacheData initializes the cache data of each record\n     * @param  array  $data\n     * @return array\n     */\n    protected function processInitCacheData($data)\n    {\n        if ($data) {\n            $model = get_class($this->model);\n\n            foreach ($data as &$record) {\n                $model::initCacheItem($record);\n            }\n        }\n\n        return $data;\n    }\n\n    /**\n     * __call handles dynamic method calls into the method\n     * @param  string  $method\n     * @param  array   $parameters\n     * @return mixed\n     *\n     * @throws \\BadMethodCallException\n     */\n    public function __call($method, $parameters)\n    {\n        $className = static::class;\n\n        throw new BadMethodCallException(\"Call to undefined method {$className}::{$method}()\");\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/Collection.php",
    "content": "<?php namespace October\\Rain\\Halcyon;\n\nuse October\\Rain\\Support\\Collection as CollectionBase;\n\n/**\n * This class represents a collection of Halcyon models.\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Collection extends CollectionBase\n{\n}\n"
  },
  {
    "path": "src/Halcyon/Concerns/HasEvents.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Concerns;\n\nuse Illuminate\\Contracts\\Events\\Dispatcher;\n\n/**\n * HasEvents concern for a model\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait HasEvents\n{\n    /**\n     * @var array eventsBooted is the array of models booted events.\n     */\n    protected static $eventsBooted = [];\n\n    /**\n     * @var \\Illuminate\\Contracts\\Events\\Dispatcher dispatcher instance\n     */\n    protected static $dispatcher;\n\n    /**\n     * @var array observables are user exposed observable events.\n     */\n    protected $observables = [];\n\n    /**\n     * bootNicerEvents binds some nicer events to this model, in the format of method overrides.\n     */\n    protected function bootNicerEvents()\n    {\n        if (isset(static::$eventsBooted[static::class])) {\n            return;\n        }\n\n        $nicerEvents = [\n            'creating' => 'beforeCreate',\n            'created' => 'afterCreate',\n            'saving' => 'beforeSave',\n            'saved' => 'afterSave',\n            'updating' => 'beforeUpdate',\n            'updated' => 'afterUpdate',\n            'deleting' => 'beforeDelete',\n            'deleted' => 'afterDelete',\n            'fetching' => 'beforeFetch',\n            'fetched' => 'afterFetch',\n        ];\n\n        foreach ($nicerEvents as $eventMethod => $method) {\n            self::registerModelEvent($eventMethod, function ($model) use ($method) {\n                $model->fireEvent(\"model.{$method}\");\n                return $model->$method();\n            });\n        }\n\n        // Boot event\n        $this->fireEvent('model.afterBoot');\n        $this->afterBoot();\n\n        static::$eventsBooted[static::class] = true;\n    }\n\n    /**\n     * initializeModelEvent is called every time the model is constructed.\n     */\n    protected function initializeModelEvent()\n    {\n        $this->fireEvent('model.afterInit');\n        $this->afterInit();\n    }\n\n    /**\n     * flushEventListeners removes all of the event listeners for the model.\n     */\n    public static function flushEventListeners()\n    {\n        if (!isset(static::$dispatcher)) {\n            return;\n        }\n\n        $instance = new static;\n\n        foreach ($instance->getObservableEvents() as $event) {\n            static::$dispatcher->forget(\"halcyon.{$event}: \".static::class);\n        }\n\n        static::$eventsBooted = [];\n    }\n\n    /**\n     * getObservableEvents names.\n     * @return array\n     */\n    public function getObservableEvents()\n    {\n        return array_merge(\n            [\n                'creating', 'created', 'updating', 'updated',\n                'deleting', 'deleted', 'saving', 'saved',\n                'fetching', 'fetched'\n            ],\n            $this->observables\n        );\n    }\n\n\n    /**\n     * setObservableEvents names.\n     * @param  array  $observables\n     * @return $this\n     */\n    public function setObservableEvents(array $observables)\n    {\n        $this->observables = $observables;\n\n        return $this;\n    }\n\n    /**\n     * addObservableEvents name.\n     * @param  array|mixed  $observables\n     * @return void\n     */\n    public function addObservableEvents($observables)\n    {\n        $observables = is_array($observables) ? $observables : func_get_args();\n\n        $this->observables = array_unique(array_merge($this->observables, $observables));\n    }\n\n    /**\n     * removeObservableEvents name.\n     * @param  array|mixed  $observables\n     * @return void\n     */\n    public function removeObservableEvents($observables)\n    {\n        $observables = is_array($observables) ? $observables : func_get_args();\n\n        $this->observables = array_diff($this->observables, $observables);\n    }\n\n    /**\n     * getEventDispatcher instance.\n     * @return \\Illuminate\\Contracts\\Events\\Dispatcher\n     */\n    public static function getEventDispatcher()\n    {\n        return static::$dispatcher;\n    }\n\n    /**\n     * setEventDispatcher instance.\n     * @param  \\Illuminate\\Contracts\\Events\\Dispatcher  $dispatcher\n     * @return void\n     */\n    public static function setEventDispatcher(Dispatcher $dispatcher)\n    {\n        static::$dispatcher = $dispatcher;\n    }\n\n    /**\n     * unsetEventDispatcher for models.\n     * @return void\n     */\n    public static function unsetEventDispatcher()\n    {\n        static::$dispatcher = null;\n    }\n\n    /**\n     * registerModelEvent with the dispatcher.\n     * @param  string  $event\n     * @param  \\Closure|string  $callback\n     * @param  int  $priority\n     * @return void\n     */\n    protected static function registerModelEvent($event, $callback, $priority = 0)\n    {\n        if (isset(static::$dispatcher)) {\n            $name = static::class;\n\n            static::$dispatcher->listen(\"halcyon.{$event}: {$name}\", $callback, $priority);\n        }\n    }\n\n    /**\n     * fireModelEvent for the model.\n     * @param  string  $event\n     * @param  bool  $halt\n     * @return mixed\n     */\n    protected function fireModelEvent($event, $halt = true)\n    {\n        if (!isset(static::$dispatcher)) {\n            return true;\n        }\n\n        // We will append the names of the class to the event to distinguish it from\n        // other model events that are fired, allowing us to listen on each model\n        // event set individually instead of catching event for all the models.\n        $event = \"halcyon.{$event}: \".static::class;\n\n        $method = $halt ? 'until' : 'dispatch';\n\n        return static::$dispatcher->$method($event, $this);\n    }\n\n    /**\n     * Create a new native event for handling beforeFetch().\n     * @param Closure|string $callback\n     * @return void\n     */\n    public static function fetching($callback)\n    {\n        static::registerModelEvent('fetching', $callback);\n    }\n\n    /**\n     * Create a new native event for handling afterFetch().\n     * @param Closure|string $callback\n     * @return void\n     */\n    public static function fetched($callback)\n    {\n        static::registerModelEvent('fetched', $callback);\n    }\n\n    /**\n     * Register a saving model event with the dispatcher.\n     *\n     * @param  \\Closure|string  $callback\n     * @param  int  $priority\n     * @return void\n     */\n    public static function saving($callback, $priority = 0)\n    {\n        static::registerModelEvent('saving', $callback, $priority);\n    }\n\n    /**\n     * Register a saved model event with the dispatcher.\n     *\n     * @param  \\Closure|string  $callback\n     * @param  int  $priority\n     * @return void\n     */\n    public static function saved($callback, $priority = 0)\n    {\n        static::registerModelEvent('saved', $callback, $priority);\n    }\n\n    /**\n     * Register an updating model event with the dispatcher.\n     *\n     * @param  \\Closure|string  $callback\n     * @param  int  $priority\n     * @return void\n     */\n    public static function updating($callback, $priority = 0)\n    {\n        static::registerModelEvent('updating', $callback, $priority);\n    }\n\n    /**\n     * Register an updated model event with the dispatcher.\n     *\n     * @param  \\Closure|string  $callback\n     * @param  int  $priority\n     * @return void\n     */\n    public static function updated($callback, $priority = 0)\n    {\n        static::registerModelEvent('updated', $callback, $priority);\n    }\n\n    /**\n     * Register a creating model event with the dispatcher.\n     *\n     * @param  \\Closure|string  $callback\n     * @param  int  $priority\n     * @return void\n     */\n    public static function creating($callback, $priority = 0)\n    {\n        static::registerModelEvent('creating', $callback, $priority);\n    }\n\n    /**\n     * Register a created model event with the dispatcher.\n     *\n     * @param  \\Closure|string  $callback\n     * @param  int  $priority\n     * @return void\n     */\n    public static function created($callback, $priority = 0)\n    {\n        static::registerModelEvent('created', $callback, $priority);\n    }\n\n    /**\n     * Register a deleting model event with the dispatcher.\n     *\n     * @param  \\Closure|string  $callback\n     * @param  int  $priority\n     * @return void\n     */\n    public static function deleting($callback, $priority = 0)\n    {\n        static::registerModelEvent('deleting', $callback, $priority);\n    }\n\n    /**\n     * Register a deleted model event with the dispatcher.\n     *\n     * @param  \\Closure|string  $callback\n     * @param  int  $priority\n     * @return void\n     */\n    public static function deleted($callback, $priority = 0)\n    {\n        static::registerModelEvent('deleted', $callback, $priority);\n    }\n\n\n    /**\n     * afterBoot is called after the model is constructed for the first time.\n     */\n    protected function afterBoot()\n    {\n        /**\n         * @event model.afterBoot\n         * Called after the model is booted\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.afterBoot', function () use (\\October\\Rain\\Halcyon\\Model $model) {\n         *         \\Log::info(get_class($model) . ' has booted');\n         *     });\n         *\n         */\n    }\n\n    /**\n     * afterInit is called after the model is constructed, a nicer version\n     * of overriding the __construct method.\n     */\n    protected function afterInit()\n    {\n        /**\n         * @event model.afterInit\n         * Called after the model is initialized\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.afterInit', function () use (\\October\\Rain\\Halcyon\\Model $model) {\n         *         \\Log::info(get_class($model) . ' has initialized');\n         *     });\n         *\n         */\n    }\n\n    /**\n     * beforeCreate handles the \"creating\" model event\n     */\n    protected function beforeCreate()\n    {\n        /**\n         * @event model.beforeCreate\n         * Called before the model is created\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.beforeCreate', function () use (\\October\\Rain\\Halcyon\\Model $model) {\n         *         if (!$model->isValid()) {\n         *             throw new \\Exception(\"Invalid Model!\");\n         *         }\n         *     });\n         *\n         */\n    }\n\n    /**\n     * afterCreate handles the \"created\" model event\n     */\n    protected function afterCreate()\n    {\n        /**\n         * @event model.afterCreate\n         * Called after the model is created\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.afterCreate', function () use (\\October\\Rain\\Halcyon\\Model $model) {\n         *         \\Log::info(\"{$model->name} was created!\");\n         *     });\n         *\n         */\n    }\n\n    /**\n     * beforeUpdate handles the \"updating\" model event\n     */\n    protected function beforeUpdate()\n    {\n        /**\n         * @event model.beforeUpdate\n         * Called before the model is updated\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.beforeUpdate', function () use (\\October\\Rain\\Halcyon\\Model $model) {\n         *         if (!$model->isValid()) {\n         *             throw new \\Exception(\"Invalid Model!\");\n         *         }\n         *     });\n         *\n         */\n    }\n\n    /**\n     * afterUpdate handles the \"updated\" model event\n     */\n    protected function afterUpdate()\n    {\n        /**\n         * @event model.afterUpdate\n         * Called after the model is updated\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.afterUpdate', function () use (\\October\\Rain\\Halcyon\\Model $model) {\n         *         if ($model->title !== $model->original['title']) {\n         *             \\Log::info(\"{$model->name} updated its title!\");\n         *         }\n         *     });\n         *\n         */\n    }\n\n    /**\n     * beforeSave handles the \"saving\" model event\n     */\n    protected function beforeSave()\n    {\n        /**\n         * @event model.beforeSave\n         * Called before the model is created or updated\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.beforeSave', function () use (\\October\\Rain\\Halcyon\\Model $model) {\n         *         if (!$model->isValid()) {\n         *             throw new \\Exception(\"Invalid Model!\");\n         *         }\n         *     });\n         *\n         */\n    }\n\n    /**\n     * afterSave handles the \"saved\" model event\n     */\n    protected function afterSave()\n    {\n        /**\n         * @event model.afterSave\n         * Called after the model is created or updated\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.afterSave', function () use (\\October\\Rain\\Halcyon\\Model $model) {\n         *         if ($model->title !== $model->original['title']) {\n         *             \\Log::info(\"{$model->name} updated its title!\");\n         *         }\n         *     });\n         *\n         */\n    }\n\n    /**\n     * beforeDelete handles the \"deleting\" model event\n     */\n    protected function beforeDelete()\n    {\n        /**\n         * @event model.beforeDelete\n         * Called before the model is deleted\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.beforeDelete', function () use (\\October\\Rain\\Halcyon\\Model $model) {\n         *         if (!$model->isAllowedToBeDeleted()) {\n         *             throw new \\Exception(\"You cannot delete me!\");\n         *         }\n         *     });\n         *\n         */\n    }\n\n    /**\n     * afterDelete handles the \"deleted\" model event\n     */\n    protected function afterDelete()\n    {\n        /**\n         * @event model.afterDelete\n         * Called after the model is deleted\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.afterDelete', function () use (\\October\\Rain\\Halcyon\\Model $model) {\n         *         \\Log::info(\"{$model->name} was deleted\");\n         *     });\n         *\n         */\n    }\n\n    /**\n     * beforeFetch handles the \"fetching\" model event\n     */\n    protected function beforeFetch()\n    {\n        /**\n         * @event model.beforeFetch\n         * Called before the model is fetched\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.beforeFetch', function () use (\\October\\Rain\\Halcyon\\Model $model) {\n         *         if (!\\Auth::getUser()->hasAccess('fetch.this.model')) {\n         *             throw new \\Exception(\"You shall not pass!\");\n         *         }\n         *     });\n         *\n         */\n    }\n\n    /**\n     * afterFetch handles the \"fetched\" model event\n     */\n    protected function afterFetch()\n    {\n        /**\n         * @event model.afterFetch\n         * Called after the model is fetched\n         *\n         * Example usage:\n         *\n         *     $model->bindEvent('model.afterFetch', function () use (\\October\\Rain\\Halcyon\\Model $model) {\n         *         \\Log::info(\"{$model->name} was retrieved from the database\");\n         *     });\n         *\n         */\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/Datasource/AutoDatasource.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Datasource;\n\nuse Illuminate\\Support\\Arr;\nuse October\\Rain\\Halcyon\\Model;\nuse October\\Rain\\Halcyon\\Processors\\Processor;\n\n/**\n * AutoDatasource loads templates from multiple data sources\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass AutoDatasource extends Datasource implements DatasourceInterface\n{\n    /**\n     * @var array datasources\n     */\n    protected $datasources;\n\n    /**\n     * @var Datasource primaryDatasource\n     */\n    protected $primaryDatasource;\n\n    /**\n     * __construct create a new datasource instance\n     */\n    public function __construct(array $datasources)\n    {\n        $this->datasources = $datasources;\n\n        $this->primaryDatasource = Arr::first($datasources);\n\n        $this->postProcessor = new Processor;\n    }\n\n    /**\n     * hasTemplate checks if a template is found in the datasource\n     */\n    public function hasTemplate(string $dirName, string $fileName, string $extension): bool\n    {\n        foreach ($this->datasources as $datasource) {\n            if ($datasource->hasTemplate($dirName, $fileName, $extension)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * selectOne returns a single template\n     */\n    public function selectOne(string $dirName, string $fileName, string $extension)\n    {\n        foreach ($this->datasources as $source) {\n            if (!$source->hasTemplate($dirName, $fileName, $extension)) {\n                continue;\n            }\n\n            return $source->selectOne($dirName, $fileName, $extension);\n        }\n\n        return null;\n    }\n\n    /**\n     * select returns all templates\n     */\n    public function select(string $dirName, array $options = []): array\n    {\n        $result = [];\n\n        // Build from the ground up\n        foreach (array_reverse($this->datasources) as $datasource) {\n            $result = array_merge($result, $datasource->select($dirName, $options));\n        }\n\n        return collect($result)->keyBy('fileName')->all();\n    }\n\n    /**\n     * insert creates a new template\n     */\n    public function insert(string $dirName, string $fileName, string $extension, string $content): bool\n    {\n        return $this->primaryDatasource->insert($dirName, $fileName, $extension, $content);\n    }\n\n    /**\n     * update an existing template\n     */\n    public function update(string $dirName, string $fileName, string $extension, string $content, $oldFileName = null, $oldExtension = null): int\n    {\n        $findFileName = $oldFileName ?: $fileName;\n        $findExt = $oldExtension ?: $extension;\n\n        if ($this->primaryDatasource->selectOne($dirName, $findFileName, $findExt)) {\n            $result = $this->primaryDatasource->update($dirName, $fileName, $extension, $content, $oldFileName, $oldExtension);\n        }\n        else {\n            $result = $this->primaryDatasource->insert($dirName, $fileName, $extension, $content);\n        }\n\n        return $result;\n    }\n\n    /**\n     * delete against the datasource\n     */\n    public function delete(string $dirName, string $fileName, string $extension): bool\n    {\n        return $this->primaryDatasource->delete($dirName, $fileName, $extension);\n    }\n\n    /**\n     * forceDelete against the datasource, forcing the complete removal of the template\n     */\n    public function forceDelete(string $dirName, string $fileName, string $extension): bool\n    {\n        $result = false;\n\n        foreach ($this->datasources as $datasource) {\n            if ($datasource->delete($dirName, $fileName, $extension)) {\n                $result = true;\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * lastModified returns the last modified date of an object\n     */\n    public function lastModified(string $dirName, string $fileName, string $extension): ?int\n    {\n        foreach ($this->datasources as $source) {\n            if (!$source->hasTemplate($dirName, $fileName, $extension)) {\n                continue;\n            }\n\n            return $source->lastModified($dirName, $fileName, $extension);\n        }\n\n        return null;\n    }\n\n    /**\n     * makeCacheKey unique to this datasource\n     */\n    public function makeCacheKey(string $name = ''): string\n    {\n        $key = '';\n\n        foreach ($this->datasources as $datasource) {\n            $key .= $datasource->makeCacheKey($name) . '-';\n        }\n\n        $key .= $name;\n\n        return (string) crc32($key);\n    }\n\n    //\n    // Models\n    //\n\n    /**\n     * hasIndex returns true if the specified index exists in datasources\n     */\n    public function hasIndex(int $index)\n    {\n        return array_key_exists($index, $this->datasources);\n    }\n\n    /**\n     * hasModelAtIndex\n     */\n    public function hasModelAtIndex($index, Model $model): bool\n    {\n        if (!$this->hasIndex($index)) {\n            return false;\n        }\n\n        // Get the path parts\n        $dirName = $model->getObjectTypeDirName();\n        list($fileName, $extension) = $model->getFileNameParts();\n\n        // Model doesn't exist\n        if ($fileName === null) {\n            return false;\n        }\n\n        return $this->datasources[$index]->hasTemplate($dirName, $fileName, $extension);\n    }\n\n    /**\n     * updateModelAtIndex updates at a specific datasource index\n     */\n    public function updateModelAtIndex(int $index, Model $model): int\n    {\n        if (!$this->hasIndex($index)) {\n            return 0;\n        }\n\n        // Get the path parts\n        $dirName = $model->getObjectTypeDirName();\n        list($fileName, $extension) = $model->getFileNameParts();\n\n        $datasource = $this->datasources[$index];\n\n        // Get the file content\n        $content = $datasource->getPostProcessor()->processUpdate($model->newQuery(), []);\n\n        return $datasource->update($dirName, $fileName, $extension, $content);\n    }\n\n    /**\n     * deleteModelAtIndex against a specific datasource index\n     */\n    public function forceDeleteModelAtIndex(int $index, Model $model): bool\n    {\n        if (!$this->hasIndex($index)) {\n            return false;\n        }\n\n        // Get the path parts\n        $dirName = $model->getObjectTypeDirName();\n        list($fileName, $extension) = $model->getFileNameParts();\n\n        return $this->datasources[$index]->forceDelete($dirName, $fileName, $extension);\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/Datasource/Datasource.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Datasource;\n\nuse October\\Rain\\Halcyon\\Processors\\Processor;\n\n/**\n * Datasource base class\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Datasource\n{\n    use \\October\\Rain\\Support\\Traits\\Emitter;\n\n    /**\n     * @var bool forceDeleting indicates if the record is currently being force deleted\n     */\n    protected $forceDeleting = false;\n\n    /**\n     * @var \\October\\Rain\\Halcyon\\Processors\\Processor\n     */\n    protected $postProcessor;\n\n    /**\n     * getPostProcessor used by the connection\n     */\n    public function getPostProcessor(): Processor\n    {\n        return $this->postProcessor;\n    }\n\n    /**\n     * delete against the datasource\n     */\n    public function delete(string $dirName, string $fileName, string $extension): bool\n    {\n        return true;\n    }\n\n    /**\n     * forceDelete a record against the datasource\n     */\n    public function forceDelete(string $dirName, string $fileName, string $extension): bool\n    {\n        $this->forceDeleting = true;\n\n        $result = $this->delete($dirName, $fileName, $extension);\n\n        $this->forceDeleting = false;\n\n        return $result;\n    }\n\n    /**\n     * makeCacheKey unique to this datasource\n     */\n    public function makeCacheKey(string $name = ''): string\n    {\n        return (string) crc32($name);\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/Datasource/DatasourceInterface.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Datasource;\n\n/**\n * DatasourceInterface\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\ninterface DatasourceInterface\n{\n    /**\n     * hasTemplate checks if a template is found in the datasource\n     */\n    public function hasTemplate(string $dirName, string $fileName, string $extension): bool;\n\n    /**\n     * selectOne returns a single template\n     */\n    public function selectOne(string $dirName, string $fileName, string $extension);\n\n    /**\n     * select returns all templates\n     */\n    public function select(string $dirName, array $options = []): array;\n\n    /**\n     * insert creates a new template\n     */\n    public function insert(string $dirName, string $fileName, string $extension, string $content): bool;\n\n    /**\n     * update an existing template\n     */\n    public function update(string $dirName, string $fileName, string $extension, string $content, $oldFileName = null, $oldExtension = null): int;\n\n    /**\n     * delete against the datasource\n     */\n    public function delete(string $dirName, string $fileName, string $extension): bool;\n\n    /**\n     * forceDelete against the datasource, forcing the complete removal of the template\n     */\n    public function forceDelete(string $dirName, string $fileName, string $extension): bool;\n\n    /**\n     * lastModified returns the last modified date of an object\n     */\n    public function lastModified(string $dirName, string $fileName, string $extension): ?int;\n\n    /**\n     * makeCacheKey unique to this datasource\n     */\n    public function makeCacheKey(string $name = ''): string;\n}\n"
  },
  {
    "path": "src/Halcyon/Datasource/DbDatasource.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Datasource;\n\nuse Db;\nuse October\\Rain\\Halcyon\\Processors\\Processor;\nuse October\\Rain\\Halcyon\\Exception\\CreateFileException;\nuse October\\Rain\\Halcyon\\Exception\\DeleteFileException;\nuse October\\Rain\\Halcyon\\Exception\\FileExistsException;\nuse Carbon\\Carbon;\nuse Exception;\n\n/**\n * DbDatasource looks at the database for templates\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass DbDatasource extends Datasource implements DatasourceInterface\n{\n    /**\n     * @var string source identifier for this datasource instance\n     */\n    protected $source;\n\n    /**\n     * @var string table name of the datasource\n     */\n    protected $table;\n\n    /**\n     * @var array pathCache\n     */\n    protected static $pathCache = [];\n\n    /**\n     * @var array|null mtimeCache\n     */\n    protected static $mtimeCache = [];\n\n    /**\n     * __construct a new datasource instance\n     */\n    public function __construct(string $source, string $table)\n    {\n        $this->source = $source;\n\n        $this->table = $table;\n\n        $this->postProcessor = new Processor;\n    }\n\n    /**\n     * hasTemplate checks if a template is found in the datasource\n     */\n    public function hasTemplate(string $dirName, string $fileName, string $extension): bool\n    {\n        return (bool) $this->lastModified($dirName, $fileName, $extension);\n    }\n\n    /**\n     * selectOne returns a single template\n     */\n    public function selectOne(string $dirName, string $fileName, string $extension)\n    {\n        $path = $this->makeFilePath($dirName, $fileName, $extension);\n\n        if (isset(self::$pathCache[$this->source][$path])) {\n            $result = self::$pathCache[$this->source][$path];\n        }\n        else {\n            $result = $this->getQuery()->where('path', $path)->first();\n        }\n\n        if (!$result) {\n            return $result;\n        }\n\n        return [\n            'fileName' => $fileName . '.' . $extension,\n            'content' => $result->content,\n            'mtime' => Carbon::parse($result->updated_at)->timestamp,\n            'record' => $result\n        ];\n    }\n\n    /**\n     * select returns all templates, with availableoptions:\n     *\n     * - columns: only return specific columns, eg: ['fileName', 'mtime', 'content']\n     * - extensions: extensions to search for, eg: ['htm', 'md', 'twig']\n     * - fileMatch: pattern to match the filename against using the fnmatch function, eg: *gr[ae]y\n     */\n    public function select(string $dirName, array $options = []): array\n    {\n        $result = [];\n\n        extract(array_merge([\n            'columns' => null,\n            'extensions' => null,\n            'fileMatch' => null,\n        ], $options));\n\n        if ($columns === ['*'] || !is_array($columns)) {\n            $columns = null;\n        }\n\n        // Apply the dirName query\n        $query = $this->getQuery()->where('path', 'like', $dirName . '%');\n\n        // Apply the extensions filter\n        if (is_array($extensions) && !empty($extensions)) {\n            $query->where(function ($query) use ($extensions) {\n                // Get the first extension to query for\n                $query->where('path', 'like', '%' . '.' . array_pop($extensions));\n\n                if (count($extensions)) {\n                    foreach ($extensions as $ext) {\n                        $query->orWhere('path', 'like', '%' . '.' . $ext);\n                    }\n                }\n            });\n        }\n\n        // Retrieve the results\n        $results = $query->get();\n\n        foreach ($results as $item) {\n            self::$pathCache[$this->source][$item->path] = $item;\n\n            $resultItem = [];\n            $fileName = ltrim(str_replace($dirName, '', $item->path), '/');\n\n            // Apply the fileMatch filter\n            if (!empty($fileMatch) && !fnmatch($fileMatch, $fileName)) {\n                continue;\n            }\n\n            // Apply the columns filter on the data returned\n            if ($columns === null) {\n                $resultItem = [\n                    'fileName' => $fileName,\n                    'content' => $item->content,\n                    'mtime' => Carbon::parse($item->updated_at)->timestamp,\n                    'record' => $item,\n                ];\n            }\n            else {\n                if (in_array('fileName', $columns)) {\n                    $resultItem['fileName'] = $fileName;\n                }\n\n                if (in_array('content', $columns)) {\n                    $resultItem['content'] = $item->content;\n                }\n\n                if (in_array('mtime', $columns)) {\n                    $resultItem['mtime'] = Carbon::parse($item->updated_at)->timestamp;\n                }\n\n                if (in_array('record', $columns)) {\n                    $resultItem['record'] = $item;\n                }\n            }\n\n            $result[] = $resultItem;\n        }\n\n        return $result;\n    }\n\n    /**\n     * insert creates a new template\n     */\n    public function insert(string $dirName, string $fileName, string $extension, string $content): bool\n    {\n        $path = $this->makeFilePath($dirName, $fileName, $extension);\n\n        if ($this->getQuery()->where('path', $path)->count() > 0) {\n            throw (new FileExistsException())->setInvalidPath($path);\n        }\n\n        // Update a trashed record\n        if ($this->getQuery(false)->where('path', $path)->first()) {\n            return $this->update($dirName, $fileName, $extension, $content);\n        }\n\n        try {\n            $record = [\n                'source' => $this->source,\n                'path' => $path,\n                'content' => $content,\n                'file_size' => mb_strlen($content, '8bit'),\n                'updated_at' => Carbon::now()->toDateTimeString(),\n                'deleted_at' => null,\n            ];\n\n            /**\n             * @event halcyon.datasource.db.beforeInsert\n             * Provides an opportunity to modify records before being inserted into the DB\n             *\n             * Example usage:\n             *\n             *     $datasource->bindEvent('halcyon.datasource.db.beforeInsert', function ((array) &$record) {\n             *         // Attach a site id to every record in a multi-tenant application\n             *         $record['site_id'] = SiteManager::getSite()->id;\n             *     });\n             *\n             */\n            $this->fireEvent('halcyon.datasource.db.beforeInsert', [&$record]);\n\n            $this->getBaseQuery()->insert($record);\n\n            $this->flushCache();\n\n            return $record['file_size'];\n        }\n        catch (Exception $ex) {\n            throw (new CreateFileException)->setInvalidPath($path);\n        }\n    }\n\n    /**\n     * update an existing template\n     */\n    public function update(string $dirName, string $fileName, string $extension, string $content, $oldFileName = null, $oldExtension = null): int\n    {\n        $path = $this->makeFilePath($dirName, $fileName, $extension);\n\n        // Check if this file has been renamed\n        if ($oldFileName !== null) {\n            $fileName = $oldFileName;\n        }\n        if ($oldExtension !== null) {\n            $extension = $oldExtension;\n        }\n\n        $oldPath = $this->makeFilePath($dirName, $fileName, $extension);\n\n        try {\n            $fileSize = mb_strlen($content, '8bit');\n\n            $data = [\n                'path' => $path,\n                'content' => $content,\n                'file_size' => $fileSize,\n                'updated_at' => Carbon::now()->toDateTimeString(),\n                'deleted_at' => null\n            ];\n\n            /**\n             * @event halcyon.datasource.db.beforeUpdate\n             * Provides an opportunity to modify records before being updated into the DB\n             *\n             * Example usage:\n             *\n             *     $datasource->bindEvent('halcyon.datasource.db.beforeUpdate', function ((array) &$data) {\n             *         // Attach a site id to every record in a multi-tenant application\n             *         $data['site_id'] = SiteManager::getSite()->id;\n             *     });\n             *\n             */\n            $this->fireEvent('halcyon.datasource.db.beforeUpdate', [&$data]);\n\n            $this->getQuery(false)->where('path', $oldPath)->update($data);\n\n            $this->flushCache();\n\n            return $fileSize;\n        }\n        catch (Exception $ex) {\n            throw (new CreateFileException)->setInvalidPath($path);\n        }\n    }\n\n    /**\n     * delete against the datasource\n     */\n    public function delete(string $dirName, string $fileName, string $extension): bool\n    {\n        try {\n            $path = $this->makeFilePath($dirName, $fileName, $extension);\n            $recordQuery = $this->getQuery()->where('path', $path);\n\n            if ($this->forceDeleting) {\n                $result = $recordQuery->delete();\n            }\n            else {\n                $result = $recordQuery->update(['deleted_at' => Carbon::now()->toDateTimeString()]);\n            }\n\n            $this->flushCache();\n\n            return (bool) $result;\n        }\n        catch (Exception $ex) {\n            throw (new DeleteFileException)->setInvalidPath($path);\n        }\n    }\n\n    /**\n     * lastModified date of an object\n     */\n    public function lastModified(string $dirName, string $fileName, string $extension): ?int\n    {\n        try {\n            if (!isset(self::$mtimeCache[$this->source])) {\n                self::$mtimeCache[$this->source] = $this->getQuery()->pluck('updated_at', 'path')->all();\n            }\n\n            $path = $this->makeFilePath($dirName, $fileName, $extension);\n            if (!isset(self::$mtimeCache[$this->source][$path])) {\n                return null;\n            }\n\n            $result = self::$mtimeCache[$this->source][$path];\n            return Carbon::parse($result)->timestamp;\n        }\n        catch (Exception $ex) {\n            return null;\n        }\n    }\n\n    /**\n     * makeCacheKey unique to this datasource\n     */\n    public function makeCacheKey(string $name = ''): string\n    {\n        return (string) crc32($this->source . $name);\n    }\n\n    /**\n     * getBaseQuery builder object\n     */\n    protected function getBaseQuery()\n    {\n        return Db::table($this->table);\n    }\n\n    /**\n     * getQuery object\n     */\n    protected function getQuery(bool $withTrashed = true)\n    {\n        $query = $this->getBaseQuery();\n        $query->where('source', $this->source);\n\n        if ($withTrashed) {\n            $query->whereNull('deleted_at');\n        }\n\n        /**\n         * @event halcyon.datasource.db.extendQuery\n         * Provides an opportunity to modify the query object used by the Halycon DbDatasource\n         *\n         * Example usage:\n         *\n         *     $datasource->bindEvent('halcyon.datasource.db.extendQuery', function ((QueryBuilder) $query, (bool) $withTrashed) {\n         *         // Apply a site filter in a multi-tenant application\n         *         $query->where('site_id', SiteManager::getSite()->id);\n         *     });\n         *\n         */\n        $this->fireEvent('halcyon.datasource.db.extendQuery', [$query, $withTrashed]);\n\n        return $query;\n    }\n\n    /**\n     * makeFilePath helper to make file path\n     */\n    protected function makeFilePath(string $dirName, string $fileName, string $extension): string\n    {\n        return $dirName . '/' . $fileName . '.' . $extension;\n    }\n\n    /**\n     * flushCache\n     */\n    protected function flushCache()\n    {\n        unset(self::$pathCache[$this->source]);\n        unset(self::$mtimeCache[$this->source]);\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/Datasource/FileDatasource.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Datasource;\n\nuse October\\Rain\\Filesystem\\Filesystem;\nuse October\\Rain\\Halcyon\\Processors\\Processor;\nuse October\\Rain\\Halcyon\\Exception\\CreateFileException;\nuse October\\Rain\\Halcyon\\Exception\\DeleteFileException;\nuse October\\Rain\\Halcyon\\Exception\\FileExistsException;\nuse October\\Rain\\Halcyon\\Exception\\CreateDirectoryException;\nuse RecursiveDirectoryIterator;\nuse RecursiveIteratorIterator;\nuse Exception;\n\n/**\n * FileDatasource\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass FileDatasource extends Datasource implements DatasourceInterface\n{\n    /**\n     * @var string basePath is a local path to find the datasource\n     */\n    protected $basePath;\n\n    /**\n     * @var \\October\\Rain\\Filesystem\\Filesystem\n     */\n    protected $files;\n\n    /**\n     * __construct a new datasource instance\n     */\n    public function __construct(string $basePath, Filesystem $files)\n    {\n        $this->basePath = $basePath;\n\n        $this->files = $files;\n\n        $this->postProcessor = new Processor;\n    }\n\n    /**\n     * hasTemplate checks if a template is found in the datasource\n     */\n    public function hasTemplate(string $dirName, string $fileName, string $extension): bool\n    {\n        return (bool) $this->selectOne($dirName, $fileName, $extension);\n    }\n\n    /**\n     * selectOne returns a single template\n     */\n    public function selectOne(string $dirName, string $fileName, string $extension)\n    {\n        try {\n            $path = $this->makeFilePath($dirName, $fileName, $extension);\n\n            return [\n                'fileName' => $fileName . '.' . $extension,\n                'content' => $this->files->get($path),\n                'mtime' => $this->files->lastModified($path)\n            ];\n        }\n        catch (Exception $ex) {\n            return null;\n        }\n    }\n\n    /**\n     * select returns all templates, with available options:\n     *\n     * - columns: only return specific columns, eg: ['fileName', 'mtime', 'content']\n     * - extensions: extensions to search for, eg: ['htm', 'md', 'twig']\n     * - fileMatch: pattern to match the filename against using the fnmatch function, eg: *gr[ae]y\n     */\n    public function select(string $dirName, array $options = []): array\n    {\n        extract(array_merge([\n            'columns' => null,\n            'extensions' => null,\n            'fileMatch' => null,\n        ], $options));\n\n        $result = [];\n        $dirPath = $this->basePath . '/' . $dirName;\n\n        if (!$this->files->isDirectory($dirPath)) {\n            return $result;\n        }\n\n        if ($columns === ['*'] || !is_array($columns)) {\n            $columns = null;\n        }\n        else {\n            $columns = array_flip($columns);\n        }\n\n        $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dirPath));\n        // @todo This should come from $maxNesting defined in the model -sg\n        $it->setMaxDepth(5);\n        $it->rewind();\n\n        while ($it->valid()) {\n            if (!$it->isFile()) {\n                $it->next();\n                continue;\n            }\n\n            // Filter by extension\n            //\n            $fileExt = $it->getExtension();\n            if ($extensions !== null && !in_array($fileExt, $extensions)) {\n                $it->next();\n                continue;\n            }\n\n            $fileName = $it->getBasename();\n            if ($it->getDepth() > 0) {\n                $baseName = $this->files->normalizePath(substr($it->getPath(), strlen($dirPath) + 1));\n                $fileName = $baseName . '/' . $fileName;\n            }\n\n            // Filter by file name match\n            //\n            if ($fileMatch !== null && !fnmatch($fileMatch, $fileName)) {\n                $it->next();\n                continue;\n            }\n\n            $item = [];\n\n            $path = $this->basePath . '/' . $dirName . '/' . $fileName;\n\n            $item['fileName'] = $fileName;\n\n            if (!$columns || array_key_exists('content', $columns)) {\n                $item['content'] = $this->files->get($path);\n            }\n\n            if (!$columns || array_key_exists('mtime', $columns)) {\n                $item['mtime'] = $this->files->lastModified($path);\n            }\n\n            $result[] = $item;\n\n            $it->next();\n        }\n\n        return $result;\n    }\n\n    /**\n     * insert creates a new template\n     */\n    public function insert(string $dirName, string $fileName, string $extension, string $content): bool\n    {\n        $this->validateDirectoryForSave($dirName, $fileName, $extension);\n\n        $path = $this->makeFilePath($dirName, $fileName, $extension);\n\n        if ($this->files->isFile($path)) {\n            throw (new FileExistsException)->setInvalidPath($path);\n        }\n\n        try {\n            return $this->files->put($path, $content);\n        }\n        catch (Exception $ex) {\n            throw (new CreateFileException)->setInvalidPath($path);\n        }\n    }\n\n    /**\n     * update an existing template\n     */\n    public function update(string $dirName, string $fileName, string $extension, string $content, $oldFileName = null, $oldExtension = null): int\n    {\n        $this->validateDirectoryForSave($dirName, $fileName, $extension);\n\n        $path = $this->makeFilePath($dirName, $fileName, $extension);\n\n        // The same file is safe to rename when the case is changed\n        // eg: FooBar -> foobar\n        $iFileChanged = ($oldFileName !== null && strcasecmp($oldFileName, $fileName) !== 0) ||\n            ($oldExtension !== null && strcasecmp($oldExtension, $extension) !== 0);\n\n        if ($iFileChanged && $this->files->isFile($path)) {\n            throw (new FileExistsException)->setInvalidPath($path);\n        }\n\n        // File to be renamed, as delete and recreate\n        $fileChanged = ($oldFileName !== null && strcmp($oldFileName, $fileName) !== 0) ||\n            ($oldExtension !== null && strcmp($oldExtension, $extension) !== 0);\n\n        if ($fileChanged) {\n            $this->delete($dirName, $oldFileName, $oldExtension);\n        }\n\n        try {\n            return $this->files->put($path, $content);\n        }\n        catch (Exception $ex) {\n            throw (new CreateFileException)->setInvalidPath($path);\n        }\n    }\n\n    /**\n     * delete against the datasource\n     */\n    public function delete(string $dirName, string $fileName, string $extension): bool\n    {\n        $path = $this->makeFilePath($dirName, $fileName, $extension);\n\n        try {\n            return $this->files->delete($path);\n        }\n        catch (Exception $ex) {\n            throw (new DeleteFileException)->setInvalidPath($path);\n        }\n    }\n\n    /**\n     * lastModified date of an object\n     */\n    public function lastModified(string $dirName, string $fileName, string $extension): ?int\n    {\n        try {\n            $path = $this->makeFilePath($dirName, $fileName, $extension);\n\n            return $this->files->lastModified($path);\n        }\n        catch (Exception $ex) {\n            return null;\n        }\n    }\n\n    /**\n     * validateDirectoryForSave ensures the requested file can be created in\n     * the requested directory\n     */\n    protected function validateDirectoryForSave(string $dirName, string $fileName, string $extension)\n    {\n        $path = $this->makeFilePath($dirName, $fileName, $extension);\n        $dirPath = $this->basePath . '/' . $dirName;\n\n        // Create base directory\n        if (\n            (!$this->files->exists($dirPath) || !$this->files->isDirectory($dirPath)) &&\n            !$this->files->makeDirectory($dirPath, 0755, true, true)\n        ) {\n            throw (new CreateDirectoryException)->setInvalidPath($dirPath);\n        }\n\n        // Create base file directory\n        if (($pos = strpos($fileName, '/')) !== false) {\n            $fileDirPath = dirname($path);\n\n            if (\n                !$this->files->isDirectory($fileDirPath) &&\n                !$this->files->makeDirectory($fileDirPath, 0755, true, true)\n            ) {\n                throw (new CreateDirectoryException)->setInvalidPath($fileDirPath);\n            }\n        }\n    }\n\n    /**\n     * makeFilePath helper to make file path\n     */\n    protected function makeFilePath(string $dirName, string $fileName, string $extension): string\n    {\n        return $this->basePath . '/' . $dirName . '/' .$fileName . '.' . $extension;\n    }\n\n    /**\n     * getBasePath returns the base path for this datasource\n     */\n    public function getBasePath(): string\n    {\n        return $this->basePath;\n    }\n\n    /**\n     * makeCacheKey unique to this datasource\n     */\n    public function makeCacheKey(string $name = ''): string\n    {\n        return crc32($this->basePath . $name);\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/Datasource/Resolver.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Datasource;\n\n/**\n * Resolver\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Resolver implements ResolverInterface\n{\n    /**\n     * @var array datasources registrations\n     */\n    protected $datasources = [];\n\n    /**\n     * @var string default datasource name\n     */\n    protected $default;\n\n    /**\n     * __construct a new datasource resolver instance\n     */\n    public function __construct(array $datasources = [])\n    {\n        foreach ($datasources as $name => $datasource) {\n            $this->addDatasource($name, $datasource);\n        }\n    }\n\n    /**\n     * datasource instance\n     */\n    public function datasource(?string $name = null): DatasourceInterface\n    {\n        if ($name === null) {\n            $name = $this->getDefaultDatasource();\n        }\n\n        return $this->datasources[$name];\n    }\n\n    /**\n     * addDatasource to the resolver\n     */\n    public function addDatasource(string $name, DatasourceInterface $datasource)\n    {\n        $this->datasources[$name] = $datasource;\n    }\n\n    /**\n     * hasDatasource checks if a datasource has been registered\n     */\n    public function hasDatasource(string $name): bool\n    {\n        return isset($this->datasources[$name]);\n    }\n\n    /**\n     * getDefaultDatasource name\n     */\n    public function getDefaultDatasource(): ?string\n    {\n        return $this->default;\n    }\n\n    /**\n     * setDefaultDatasource name\n     */\n    public function setDefaultDatasource(string $name)\n    {\n        $this->default = $name;\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/Datasource/ResolverInterface.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Datasource;\n\n/**\n * ResolverInterface\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\ninterface ResolverInterface\n{\n    /**\n     * datasource instance\n     */\n    public function datasource(?string $name = null): DatasourceInterface;\n\n    /**\n     * getDefaultDatasource name\n     */\n    public function getDefaultDatasource(): ?string;\n\n    /**\n     * setDefaultDatasource name\n     */\n    public function setDefaultDatasource(string $name);\n}\n"
  },
  {
    "path": "src/Halcyon/Exception/CreateDirectoryException.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Exception;\n\nuse File;\nuse RuntimeException;\n\n/**\n * CreateDirectoryException\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass CreateDirectoryException extends RuntimeException\n{\n    /**\n     * @var string invalidPath of the affected directory path\n     */\n    protected $invalidPath;\n\n    /**\n     * setInvalidPath sets the affected directory path\n     */\n    public function setInvalidPath(string $path): CreateDirectoryException\n    {\n        $this->invalidPath = $path;\n\n        $this->message = \"Error creating directory [{$path}]. Please check write permissions.\";\n\n        return $this;\n    }\n\n    /**\n     * getInvalidPath is the affected directory path\n     */\n    public function getInvalidPath(): string\n    {\n        return File::nicePath($this->invalidPath);\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/Exception/CreateFileException.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Exception;\n\nuse File;\nuse RuntimeException;\n\n/**\n * CreateFileException\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass CreateFileException extends RuntimeException\n{\n    /**\n     * @var string invalidPath of the affected directory path\n     */\n    protected $invalidPath;\n\n    /**\n     * setInvalidPath sets the affected directory path\n     */\n    public function setInvalidPath(string $path): CreateFileException\n    {\n        $this->invalidPath = $path;\n\n        $this->message = \"Error creating file [{$path}]. Please check write permissions.\";\n\n        return $this;\n    }\n\n    /**\n     * getInvalidPath is the affected directory path\n     */\n    public function getInvalidPath(): string\n    {\n        return File::nicePath($this->invalidPath);\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/Exception/DeleteFileException.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Exception;\n\nuse File;\nuse RuntimeException;\n\n/**\n * DeleteFileException\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass DeleteFileException extends RuntimeException\n{\n    /**\n     * @var string invalidPath of the affected directory path\n     */\n    protected $invalidPath;\n\n    /**\n     * setInvalidPath sets the affected directory path\n     */\n    public function setInvalidPath(string $path): DeleteFileException\n    {\n        $this->invalidPath = $path;\n\n        $this->message = \"Error deleting file [{$path}]. Please check write permissions.\";\n\n        return $this;\n    }\n\n    /**\n     * getInvalidPath is the affected directory path\n     */\n    public function getInvalidPath(): string\n    {\n        return File::nicePath($this->invalidPath);\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/Exception/FileExistsException.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Exception;\n\nuse File;\nuse RuntimeException;\n\n/**\n * FileExistsException\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass FileExistsException extends RuntimeException\n{\n    /**\n     * @var string invalidPath of the affected directory path\n     */\n    protected $invalidPath;\n\n    /**\n     * setInvalidPath sets the affected directory path\n     */\n    public function setInvalidPath(string $path): FileExistsException\n    {\n        $this->invalidPath = $path;\n\n        $this->message = \"A file already exists at [{$path}].\";\n\n        return $this;\n    }\n\n    /**\n     * getInvalidPath is the affected directory path\n     */\n    public function getInvalidPath(): string\n    {\n        return File::nicePath($this->invalidPath);\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/Exception/InvalidDirectoryNameException.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Exception;\n\nuse RuntimeException;\n\n/**\n * InvalidDirectoryNameException\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass InvalidDirectoryNameException extends RuntimeException\n{\n    /**\n     * @var string invalidDirName of the affected file name\n     */\n    protected $invalidDirName;\n\n    /**\n     * setInvalidDirectoryName sets the affected file name\n     */\n    public function setInvalidDirectoryName(string $invalidDirName): InvalidDirectoryNameException\n    {\n        $this->invalidDirName = $invalidDirName;\n\n        $this->message = \"The specified directory name [{$invalidDirName}] is invalid.\";\n\n        return $this;\n    }\n\n    /**\n     * getInvalidDirectoryName gets the affected file name\n     */\n    public function getInvalidDirectoryName(): string\n    {\n        return $this->invalidDirName;\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/Exception/InvalidExtensionException.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Exception;\n\nuse RuntimeException;\n\n/**\n * InvalidExtensionException\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass InvalidExtensionException extends RuntimeException\n{\n    /**\n     * @var string invalidExtension\n     */\n    protected $invalidExtension;\n\n    /**\n     * @var array allowedExtensions\n     */\n    protected $allowedExtensions;\n\n    /**\n     * setInvalidExtension sets the affected file extension\n     */\n    public function setInvalidExtension(string $invalidExtension): InvalidExtensionException\n    {\n        $this->invalidExtension = $invalidExtension;\n\n        $this->message = \"The specified file extension [{$invalidExtension}] is invalid.\";\n\n        return $this;\n    }\n\n    /**\n     * getInvalidExtension gets the affected file extension\n     */\n    public function getInvalidExtension(): string\n    {\n        return $this->invalidExtension;\n    }\n\n    /**\n     * setAllowedExtensions sets the list of allowed extensions\n     */\n    public function setAllowedExtensions(array $allowedExtensions): InvalidExtensionException\n    {\n        $this->allowedExtensions = $allowedExtensions;\n\n        return $this;\n    }\n\n    /**\n     * getAllowedExtensions gets the list of allowed extensions\n     */\n    public function getAllowedExtensions(): array\n    {\n        return $this->allowedExtensions;\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/Exception/InvalidFileNameException.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Exception;\n\nuse RuntimeException;\n\n/**\n * InvalidFileNameException\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass InvalidFileNameException extends RuntimeException\n{\n    /**\n     * @var string invalidFileName\n     */\n    protected $invalidFileName;\n\n    /**\n     * setInvalidFileName the affected file name\n     */\n    public function setInvalidFileName(string $invalidFileName): InvalidFileNameException\n    {\n        $this->invalidFileName = $invalidFileName;\n\n        $this->message = \"The specified file name [{$invalidFileName}] is invalid.\";\n\n        return $this;\n    }\n\n    /**\n     * getInvalidFileName gets the affected file name\n     */\n    public function getInvalidFileName(): string\n    {\n        return $this->invalidFileName;\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/Exception/MissingFileNameException.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Exception;\n\nuse RuntimeException;\n\n/**\n * MissingFileNameException\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass MissingFileNameException extends RuntimeException\n{\n    /**\n     * @var string model name\n     */\n    protected $model;\n\n    /**\n     * setModel sets the affected Halcyon model\n     */\n    public function setModel(string $model): MissingFileNameException\n    {\n        $this->model = $model;\n\n        $this->message = \"No file name attribute (fileName) specified for model [{$model}].\";\n\n        return $this;\n    }\n\n    /**\n     * getModel gets the affected Halcyon model\n     */\n    public function getModel(): string\n    {\n        return $this->model;\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/Exception/ModelException.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Exception;\n\nuse October\\Rain\\Halcyon\\Model;\nuse October\\Rain\\Exception\\ValidationException;\nuse Illuminate\\Support\\MessageBag;\nuse Exception;\n\n/**\n * ModelException used when validation fails, contains the invalid model for easy analysis\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ModelException extends ValidationException\n{\n    /**\n     * @var Model model\n     */\n    protected $model;\n\n    /**\n     * @var MessageBag validationErrors\n     */\n    protected $validationErrors;\n\n    /**\n     * __construct receives the invalid model\n     */\n    public function __construct(Model $model)\n    {\n        $this->model = $model;\n        $this->validationErrors = $model->errors();\n\n        // Bypass parent constructor to avoid Validator facade dependency\n        Exception::__construct($this->validationErrors->first());\n\n        $this->evalErrors();\n    }\n\n    /**\n     * errors returns validation errors\n     */\n    public function errors(): array\n    {\n        return $this->validationErrors->messages();\n    }\n\n    /**\n     * getErrors returns the message bag instance\n     */\n    public function getErrors(): MessageBag\n    {\n        return $this->validationErrors;\n    }\n\n    /**\n     * getModel returns the model with invalid attributes\n     */\n    public function getModel(): Model\n    {\n        return $this->model;\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/HalcyonServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Halcyon;\n\nuse October\\Rain\\Halcyon\\Datasource\\Resolver;\nuse October\\Rain\\Support\\ServiceProvider;\n\n/**\n * HalcyonServiceProvider\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass HalcyonServiceProvider extends ServiceProvider\n{\n    /**\n     * Bootstrap the application events.\n     *\n     * @return void\n     */\n    public function boot()\n    {\n        Model::setDatasourceResolver($this->app['halcyon']);\n\n        Model::setEventDispatcher($this->app['events']);\n\n        Model::setCacheManager($this->app['cache']);\n    }\n\n    /**\n     * Register the service provider.\n     *\n     * @return void\n     */\n    public function register()\n    {\n        Model::clearBootedModels();\n\n        Model::flushEventListeners();\n\n        $this->app->singleton('halcyon', function ($app) {\n            return new Resolver;\n        });\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/Migrations/2021_10_01_000001_Db_Templates.php",
    "content": "<?php\n\nuse October\\Rain\\Database\\Schema\\Blueprint;\nuse October\\Rain\\Database\\Updates\\Migration;\n\nreturn new class extends Migration\n{\n    public function up()\n    {\n        Schema::create('templates', function (Blueprint $table) {\n            $table->increments('id');\n            $table->string('source')->index();\n            $table->string('path')->index();\n            $table->longText('content');\n            $table->integer('file_size')->unsigned();\n            $table->dateTime('updated_at')->nullable();\n            $table->dateTime('deleted_at')->nullable();\n        });\n    }\n\n    public function down()\n    {\n        Schema::dropIfExists('templates');\n    }\n};\n"
  },
  {
    "path": "src/Halcyon/Model.php",
    "content": "<?php namespace October\\Rain\\Halcyon;\n\nuse October\\Rain\\Support\\Arr;\nuse October\\Rain\\Support\\Str;\nuse October\\Rain\\Extension\\Extendable;\nuse October\\Rain\\Halcyon\\Datasource\\ResolverInterface as Resolver;\nuse Illuminate\\Contracts\\Support\\Jsonable;\nuse Illuminate\\Contracts\\Support\\Arrayable;\nuse BadMethodCallException;\nuse JsonSerializable;\nuse ArrayAccess;\nuse Exception;\n\n/**\n * Model is a base template object, equivalent to a Model in ORM\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Model extends Extendable implements ArrayAccess, Arrayable, Jsonable, JsonSerializable\n{\n    use \\October\\Rain\\Support\\Traits\\Emitter;\n    use \\October\\Rain\\Halcyon\\Concerns\\HasEvents;\n\n    /**\n     * @var string datasource is the data source for the model, a directory path.\n     */\n    protected $datasource;\n\n    /**\n     * @var string dirName is the container name associated with the model, eg: pages.\n     */\n    protected $dirName;\n\n    /**\n     * @var array attributes saved to the settings area.\n     */\n    public $attributes = [];\n\n    /**\n     * @var array original attributes.\n     */\n    protected $original = [];\n\n    /**\n     * @var array appends to the model's array form.\n     */\n    protected $appends = [];\n\n    /**\n     * @var array fillable attributes are mass assignable\n     */\n    protected $fillable = [];\n\n    /**\n     * @var array List of attribute names which are not considered \"settings\".\n     */\n    protected $purgeable = [];\n\n    /**\n     * @var array allowedExtensions is allowable file extensions.\n     */\n    protected $allowedExtensions = ['htm'];\n\n    /**\n     * @var string defaultExtension is default file extension.\n     */\n    protected $defaultExtension = 'htm';\n\n    /**\n     * @var bool isCompoundObject supports code and settings sections.\n     */\n    protected $isCompoundObject = true;\n\n    /**\n     * @var bool wrapCode section in PHP tags.\n     */\n    protected $wrapCode = true;\n\n    /**\n     * @var int maxNesting is the maximum allowed path nesting level. The default value is 5,\n     * meaning that files can only exist in the root directory, or in a subdirectory.\n     * Set to null if any level is allowed.\n     */\n    protected $maxNesting = 5;\n\n    /**\n     * @var bool loadedFromCache indicates whether the object was loaded from the cache.\n     */\n    protected $loadedFromCache = false;\n\n    /**\n     * @var bool exists indicates if the model exists.\n     */\n    public $exists = false;\n\n    /**\n     * @var \\Illuminate\\Cache\\CacheManager cache manager\n     */\n    protected static $cache;\n\n    /**\n     * @var \\October\\Rain\\Halcyon\\Datasource\\ResolverInterface resolver instance.\n     */\n    protected static $resolver;\n\n    /**\n     * @var array mutatorCache for each class.\n     */\n    protected static $mutatorCache = [];\n\n    /**\n     * @var array booted models\n     */\n    protected static $booted = [];\n\n    /**\n     * @var array traitInitializers that will be called on each new instance.\n     */\n    protected static $traitInitializers = [];\n\n    /**\n     * __construct a new Halcyon model instance.\n     * @param  array  $attributes\n     * @return void\n     */\n    public function __construct(array $attributes = [])\n    {\n        $this->bootIfNotBooted();\n\n        $this->initializeTraits();\n\n        $this->bootNicerEvents();\n\n        parent::__construct();\n\n        $this->initializeModelEvent();\n\n        $this->syncOriginal();\n\n        $this->fill($attributes);\n    }\n\n    /**\n     * bootIfNotBooted checks if the model needs to be booted and if so, do it.\n     */\n    protected function bootIfNotBooted()\n    {\n        if (!isset(static::$booted[static::class])) {\n            static::$booted[static::class] = true;\n\n            $this->fireModelEvent('booting', false);\n\n            static::booting();\n            static::boot();\n            static::booted();\n\n            $this->fireModelEvent('booted', false);\n        }\n    }\n\n    /**\n     * booting performs any actions required before the model boots.\n     */\n    protected static function booting()\n    {\n        //\n    }\n\n    /**\n     * boot is the \"booting\" method of the model.\n     */\n    protected static function boot()\n    {\n        static::bootTraits();\n    }\n\n    /**\n     * bootTraits boots all of the bootable traits on the model.\n     */\n    protected static function bootTraits()\n    {\n        $class = static::class;\n\n        $booted = [];\n\n        static::$traitInitializers[$class] = [];\n\n        foreach (class_uses_recursive($class) as $trait) {\n            $method = 'boot'.class_basename($trait);\n\n            if (method_exists($class, $method) && ! in_array($method, $booted)) {\n                forward_static_call([$class, $method]);\n\n                $booted[] = $method;\n            }\n\n            if (method_exists($class, $method = 'initialize'.class_basename($trait))) {\n                static::$traitInitializers[$class][] = $method;\n\n                static::$traitInitializers[$class] = array_unique(\n                    static::$traitInitializers[$class]\n                );\n            }\n        }\n    }\n\n    /**\n     * initializeTraits on the model.\n     */\n    protected function initializeTraits()\n    {\n        foreach (static::$traitInitializers[static::class] as $method) {\n            $this->{$method}();\n        }\n    }\n\n    /**\n     * booted performs any actions required after the model boots.\n     */\n    protected static function booted()\n    {\n        //\n    }\n\n    /**\n     * clearBootedModels clears the list of booted models so they will be re-booted.\n     */\n    public static function clearBootedModels()\n    {\n        static::$booted = [];\n    }\n\n    /**\n     * getIdAttribute is a helper for {{ page.id }} or {{ layout.id }} twig vars\n     * Returns a semi-unique string for this object.\n     * @return string\n     */\n    public function getIdAttribute()\n    {\n        return str_replace('/', '-', $this->getBaseFileNameAttribute());\n    }\n\n    /**\n     * getBaseFileNameAttribute returns the file name without the extension.\n     * @return string\n     */\n    public function getBaseFileNameAttribute()\n    {\n        $pos = strrpos($this->fileName, '.');\n        if ($pos === false) {\n            return $this->fileName;\n        }\n\n        return substr($this->fileName, 0, $pos);\n    }\n\n    /**\n     * addFillable adds fillable attributes for the model.\n     * @param  array|string|null  $attributes\n     * @return void\n     */\n    public function addFillable($attributes = null)\n    {\n        $attributes = is_array($attributes) ? $attributes : func_get_args();\n\n        $this->fillable = array_merge($this->fillable, $attributes);\n    }\n\n    /**\n     * addPurgeable adds an attribute to the purgeable attributes list\n     * @param  array|string|null  $attributes\n     * @return void\n     */\n    public function addPurgeable($attributes = null)\n    {\n        $attributes = is_array($attributes) ? $attributes : func_get_args();\n\n        $this->purgeable = array_merge($this->purgeable, $attributes);\n    }\n\n    /**\n     * getSettingsAttribute is the settings is attribute contains everything that should\n     * be saved to the settings area.\n     * @return array\n     */\n    public function getSettingsAttribute()\n    {\n        $defaults = [\n            'fileName',\n            'components',\n            'content',\n            'markup',\n            'mtime',\n            'code'\n        ];\n\n        return array_diff_key(\n            $this->attributes,\n            array_flip(array_merge($defaults, $this->purgeable))\n        );\n    }\n\n    /**\n     * setSettingsAttribute filling the settings should merge it with attributes.\n     * @param mixed $value\n     */\n    public function setSettingsAttribute($value)\n    {\n        if (is_array($value)) {\n            $this->attributes = array_merge($this->attributes, $value);\n        }\n    }\n\n    /**\n     * setFileNameAttribute wjere file name should always contain an extension.\n     * @param mixed $value\n     */\n    public function setFileNameAttribute($value)\n    {\n        $fileName = trim($value);\n\n        if (strlen($fileName) && !strlen(pathinfo($value, PATHINFO_EXTENSION))) {\n            $fileName .= '.'.$this->defaultExtension;\n        }\n\n        $this->attributes['fileName'] = $fileName;\n    }\n\n    /**\n     * getObjectTypeDirName returns the directory name corresponding to the object type.\n     * For pages the directory name is \"pages\", for layouts - \"layouts\", etc.\n     * @return string\n     */\n    public function getObjectTypeDirName()\n    {\n        return $this->dirName;\n    }\n\n    /**\n     * getAllowedExtensions returns the allowable file extensions supported by this model.\n     * @return array\n     */\n    public function getAllowedExtensions()\n    {\n        return $this->allowedExtensions;\n    }\n\n    /**\n     * isCompoundObject returns true if this template supports code and settings sections.\n     * @return bool\n     */\n    public function isCompoundObject()\n    {\n        return $this->isCompoundObject;\n    }\n\n    /**\n     * getWrapCode returns true if the code section will be wrapped in PHP tags.\n     * @return bool\n     */\n    public function getWrapCode()\n    {\n        return $this->wrapCode;\n    }\n\n    /**\n     * getMaxNesting returns the maximum directory nesting allowed by this template.\n     * @return int\n     */\n    public function getMaxNesting()\n    {\n        return $this->maxNesting;\n    }\n\n    /**\n     * isLoadedFromCache returns true if the object was loaded from the cache.\n     * @return boolean\n     */\n    public function isLoadedFromCache()\n    {\n        return $this->loadedFromCache;\n    }\n\n    /**\n     * setLoadedFromCache returns true if the object was loaded from the cache.\n     * @return bool\n     */\n    public function setLoadedFromCache($value)\n    {\n        $this->loadedFromCache = (bool) $value;\n    }\n\n    /**\n     * fill the model with an array of attributes.\n     * @param  array  $attributes\n     * @return $this\n     */\n    public function fill(array $attributes)\n    {\n        foreach ($this->fillableFromArray($attributes) as $key => $value) {\n            if ($this->isFillable($key)) {\n                $this->setAttribute($key, $value);\n            }\n        }\n\n        return $this;\n    }\n\n    /**\n     * fillableFromArray gets the fillable attributes of a given array.\n     * @param  array  $attributes\n     * @return array\n     */\n    protected function fillableFromArray(array $attributes)\n    {\n        $defaults = ['fileName'];\n\n        if (count($this->fillable) > 0) {\n            return array_intersect_key(\n                $attributes,\n                array_flip(array_merge($defaults, $this->fillable))\n            );\n        }\n\n        return $attributes;\n    }\n\n    /**\n     * newInstance creates a new instance of the given model.\n     * @param  array  $attributes\n     * @param  bool  $exists\n     * @return static\n     */\n    public function newInstance($attributes = [], $exists = false)\n    {\n        // This method just provides a convenient way for us to generate fresh model\n        // instances of this current model. It is particularly useful during the\n        // hydration of new objects via the Halcyon query builder instances.\n        $model = new static((array) $attributes);\n\n        $model->exists = $exists;\n\n        return $model;\n    }\n\n    /**\n     * newFromBuilder creates a new model instance that is existing.\n     * @param  array  $attributes\n     * @param  string|null  $datasource\n     * @return static\n     */\n    public function newFromBuilder($attributes = [], $datasource = null)\n    {\n        $instance = $this->newInstance([], true);\n\n        if ($instance->fireModelEvent('fetching') === false) {\n            return $instance;\n        }\n\n        $instance->setRawAttributes((array) $attributes, true);\n\n        $instance->fireModelEvent('fetched', false);\n\n        $instance->setDatasource($datasource ?: $this->datasource);\n\n        return $instance;\n    }\n\n    /**\n     * hydrate creates a collection of models from plain arrays.\n     * @param  array  $items\n     * @param  string|null  $datasource\n     * @return \\October\\Rain\\Halcyon\\Collection\n     */\n    public static function hydrate(array $items, $datasource = null)\n    {\n        $instance = (new static)->setDatasource($datasource);\n\n        $items = array_map(function ($item) use ($instance) {\n            return $instance->newFromBuilder($item);\n        }, $items);\n\n        return $instance->newCollection($items);\n    }\n\n    /**\n     * create saves a new model and return the instance.\n     * @param  array  $attributes\n     * @return static\n     */\n    public static function create(array $attributes = [])\n    {\n        $model = new static($attributes);\n\n        $model->save();\n\n        return $model;\n    }\n\n    /**\n     * query begins querying the model.\n     * @return \\October\\Rain\\Halcyon\\Builder\n     */\n    public static function query()\n    {\n        return (new static)->newQuery();\n    }\n\n    /**\n     * on begins querying the model on a given datasource.\n     * @param  string|null  $datasource\n     * @return \\October\\Rain\\Halcyon\\Model\n     */\n    public static function on($datasource = null)\n    {\n        // First we will just create a fresh instance of this model, and then we can\n        // set the datasource on the model so that it is be used for the queries.\n        $instance = new static;\n\n        $instance->setDatasource($datasource);\n\n        return $instance;\n    }\n\n    /**\n     * all of the models from the datasource.\n     * @return \\October\\Rain\\Halcyon\\Collection|static[]\n     */\n    public static function all()\n    {\n        $instance = new static;\n\n        return $instance->newQuery()->get();\n    }\n\n    /**\n     * isFillable determines if the given attribute may be mass assigned.\n     * @param  string  $key\n     * @return bool\n     */\n    public function isFillable($key)\n    {\n        // File name is always treated as a fillable attribute.\n        if ($key === 'fileName') {\n            return true;\n        }\n\n        // If the key is in the \"fillable\" array, we can of course assume that it's\n        // a fillable attribute. Otherwise, we will check the guarded array when\n        // we need to determine if the attribute is black-listed on the model.\n        if (in_array($key, $this->fillable)) {\n            return true;\n        }\n\n        return empty($this->fillable) && !Str::startsWith($key, '_');\n    }\n\n    /**\n     * toJson converts the model instance to JSON.\n     * @param  int  $options\n     * @return string\n     */\n    public function toJson($options = 0)\n    {\n        return json_encode($this->jsonSerialize(), $options);\n    }\n\n    /**\n     * jsonSerialize converts the object into something JSON serializable.\n     */\n    public function jsonSerialize(): array\n    {\n        return $this->toArray();\n    }\n\n    /**\n     * toArray converts the model instance to an array.\n     * @return array\n     */\n    public function toArray()\n    {\n        return $this->attributesToArray();\n    }\n\n    /**\n     * attributesToArray converts the model's attributes to an array.\n     * @return array\n     */\n    public function attributesToArray()\n    {\n        $attributes = $this->attributes;\n\n        $mutatedAttributes = $this->getMutatedAttributes();\n\n        // We want to spin through all the mutated attributes for this model and call\n        // the mutator for the attribute. We cache off every mutated attributes so\n        // we don't have to constantly check on attributes that actually change.\n        foreach ($mutatedAttributes as $key) {\n            if (!array_key_exists($key, $attributes)) {\n                continue;\n            }\n\n            $attributes[$key] = $this->mutateAttributeForArray(\n                $key,\n                $attributes[$key]\n            );\n        }\n\n        // Here we will grab all of the appended, calculated attributes to this model\n        // as these attributes are not really in the attributes array, but are run\n        // when we need to array or JSON the model for convenience to the coder.\n        foreach ($this->getArrayableAppends() as $key) {\n            $attributes[$key] = $this->mutateAttributeForArray($key, null);\n        }\n\n        return $attributes;\n    }\n\n    /**\n     * getArrayableAppends gets all of the appendable values that are arrayable.\n     * @return array\n     */\n    protected function getArrayableAppends()\n    {\n        $defaults = ['settings'];\n\n        if (!count($this->appends)) {\n            return $defaults;\n        }\n\n        return array_merge($defaults, $this->appends);\n    }\n\n    /**\n     * getAttribute gets a plain attribute.\n     * @param  string  $key\n     * @return mixed\n     */\n    public function getAttribute($key)\n    {\n        // Before Event\n        if (($attr = $this->fireEvent('model.beforeGetAttribute', [$key], true)) !== null) {\n            return $attr;\n        }\n\n        $value = $this->getAttributeFromArray($key);\n\n        // If the attribute has a get mutator, we will call that then return what\n        // it returns as the value, which is useful for transforming values on\n        // retrieval from the model to a form that is more useful for usage.\n        if ($this->hasGetMutator($key)) {\n            return $this->mutateAttribute($key, $value);\n        }\n\n        // After Event\n        if (($_attr = $this->fireEvent('model.getAttribute', [$key, $value], true)) !== null) {\n            return $_attr;\n        }\n\n        return $value;\n    }\n\n    /**\n     * getAttributeFromArray gets an attribute from the $attributes array.\n     * @param  string  $key\n     * @return mixed\n     */\n    protected function getAttributeFromArray($key)\n    {\n        if (array_key_exists($key, $this->attributes)) {\n            return $this->attributes[$key];\n        }\n    }\n\n    /**\n     * hasGetMutator determines if a get mutator exists for an attribute.\n     * @param  string  $key\n     * @return bool\n     */\n    public function hasGetMutator($key)\n    {\n        return $this->methodExists('get'.Str::studly($key).'Attribute');\n    }\n\n    /**\n     * mutateAttribute gets the value of an attribute using its mutator.\n     * @param  string  $key\n     * @param  mixed  $value\n     * @return mixed\n     */\n    protected function mutateAttribute($key, $value)\n    {\n        return $this->{'get'.Str::studly($key).'Attribute'}($value);\n    }\n\n    /**\n     * mutateAttributeForArray gets the value of an attribute using its mutator for array conversion.\n     * @param  string  $key\n     * @param  mixed  $value\n     * @return mixed\n     */\n    protected function mutateAttributeForArray($key, $value)\n    {\n        $value = $this->mutateAttribute($key, $value);\n\n        return $value instanceof Arrayable ? $value->toArray() : $value;\n    }\n\n    /**\n     * setAttribute sets a given attribute on the model.\n     * @param  string  $key\n     * @param  mixed  $value\n     * @return $this\n     */\n    public function setAttribute($key, $value)\n    {\n        // Before Event\n        if (($_value = $this->fireEvent('model.beforeSetAttribute', [$key, $value], true)) !== null) {\n            $value = $_value;\n        }\n\n        // First we will check for the presence of a mutator for the set operation\n        // which simply lets the developers tweak the attribute as it is set on\n        // the model, such as \"json_encoding\" an listing of data for storage.\n        if ($this->hasSetMutator($key)) {\n            $method = 'set'.Str::studly($key).'Attribute';\n            // If we return the returned value of the mutator call straight away, that will disable the firing of\n            // 'model.setAttribute' event, and then no third party plugins will be able to implement any kind of\n            // post processing logic when an attribute is set with explicit mutators. Returning from the mutator\n            // call will also break method chaining as intended by returning `$this` at the end of this method.\n            $this->{$method}($value);\n        }\n        else {\n            $this->attributes[$key] = $value;\n        }\n\n        // After Event\n        $this->fireEvent('model.setAttribute', [$key, $value]);\n\n        return $this;\n    }\n\n    /**\n     * hasSetMutator determines if a set mutator exists for an attribute.\n     * @param  string  $key\n     * @return bool\n     */\n    public function hasSetMutator($key)\n    {\n        return $this->methodExists('set'.Str::studly($key).'Attribute');\n    }\n\n    /**\n     * getAttributes gets all of the current attributes on the model.\n     * @return array\n     */\n    public function getAttributes()\n    {\n        return $this->attributes;\n    }\n\n    /**\n     * setRawAttributes sets the array of model attributes. No checking is done.\n     * @param  array  $attributes\n     * @param  bool  $sync\n     * @return $this\n     */\n    public function setRawAttributes(array $attributes, $sync = false)\n    {\n        $this->attributes = $attributes;\n\n        if ($sync) {\n            $this->syncOriginal();\n        }\n\n        return $this;\n    }\n\n    /**\n     * getOriginal gets the model's original attribute values.\n     * @param  string|null  $key\n     * @param  mixed  $default\n     * @return array\n     */\n    public function getOriginal($key = null, $default = null)\n    {\n        return Arr::get($this->original, $key, $default);\n    }\n\n    /**\n     * syncOriginal attributes with the current.\n     * @return $this\n     */\n    public function syncOriginal()\n    {\n        $this->original = $this->attributes;\n\n        return $this;\n    }\n\n    /**\n     * syncOriginalAttribute syncs a single original attribute with its current value.\n     * @param  string  $attribute\n     * @return $this\n     */\n    public function syncOriginalAttribute($attribute)\n    {\n        $this->original[$attribute] = $this->attributes[$attribute];\n\n        return $this;\n    }\n\n    /**\n     * isDirty determines if the model or given attribute(s) have been modified.\n     * @param  array|string|null  $attributes\n     * @return bool\n     */\n    public function isDirty($attributes = null)\n    {\n        $dirty = $this->getDirty();\n\n        if (is_null($attributes)) {\n            return count($dirty) > 0;\n        }\n\n        if (!is_array($attributes)) {\n            $attributes = func_get_args();\n        }\n\n        foreach ($attributes as $attribute) {\n            if (array_key_exists($attribute, $dirty)) {\n                return true;\n            }\n        }\n\n        return false;\n    }\n\n    /**\n     * getDirty get the attributes that have been changed since last sync.\n     * @return array\n     */\n    public function getDirty()\n    {\n        $dirty = [];\n\n        foreach ($this->attributes as $key => $value) {\n            if (!array_key_exists($key, $this->original)) {\n                $dirty[$key] = $value;\n            }\n            elseif (\n                $value !== $this->original[$key] &&\n                !$this->originalIsNumericallyEquivalent($key)\n            ) {\n                $dirty[$key] = $value;\n            }\n        }\n\n        foreach ($this->original as $key => $value) {\n            if (!array_key_exists($key, $this->attributes)) {\n                $dirty[$key] = null;\n            }\n        }\n\n        return $dirty;\n    }\n\n    /**\n     * originalIsNumericallyEquivalent determine if the new and old values for a given key are\n     * numerically equivalent.\n     * @param  string  $key\n     * @return bool\n     */\n    protected function originalIsNumericallyEquivalent($key)\n    {\n        $current = $this->attributes[$key];\n\n        $original = $this->original[$key];\n\n        return is_numeric($current) && is_numeric($original) && strcmp((string) $current, (string) $original) === 0;\n    }\n\n    /**\n     * delete the model from the database.\n     * @return bool|null\n     * @throws \\Exception\n     */\n    public function delete()\n    {\n        if (is_null($this->fileName)) {\n            throw new Exception('No file name (fileName) defined on model.');\n        }\n\n        if ($this->exists) {\n            if ($this->fireModelEvent('deleting') === false) {\n                return false;\n            }\n\n            $this->performDeleteOnModel();\n\n            $this->exists = false;\n\n            // Once the model has been deleted, we will fire off the deleted event so that\n            // the developers may hook into post-delete operations. We will then return\n            // a boolean true as the delete is presumably successful on the database.\n            $this->fireModelEvent('deleted', false);\n\n            return true;\n        }\n    }\n\n    /**\n     * performDeleteOnModel performs the actual delete query on this model instance.\n     */\n    protected function performDeleteOnModel()\n    {\n        $this->newQuery()->delete($this->fileName);\n    }\n\n    /**\n     * Update the model in the database.\n     *\n     * @param  array  $attributes\n     * @return bool|int\n     */\n    public function update(array $attributes = [])\n    {\n        if (!$this->exists) {\n            return $this->newQuery()->update($attributes);\n        }\n\n        return $this->fill($attributes)->save();\n    }\n\n    /**\n     * Save the model to the datasource.\n     *\n     * @param  array  $options\n     * @return bool\n     */\n    public function save(?array $options = null)\n    {\n        return $this->saveInternal(['force' => false] + (array) $options);\n    }\n\n    /**\n     * Save the model to the database. Is used by {@link save()} and {@link forceSave()}.\n     * @param array $options\n     * @return bool\n     */\n    public function saveInternal(array $options = [])\n    {\n        // Event\n        if ($this->fireEvent('model.saveInternal', [$this->attributes, $options], true) === false) {\n            return false;\n        }\n\n        $query = $this->newQuery();\n\n        // If the \"saving\" event returns false we'll bail out of the save and return\n        // false, indicating that the save failed. This provides a chance for any\n        // listeners to cancel save operations if validations fail or whatever.\n        if ($this->fireModelEvent('saving') === false) {\n            return false;\n        }\n\n        if ($this->exists) {\n            $saved = $this->performUpdate($query, $options);\n        }\n        else {\n            $saved = $this->performInsert($query, $options);\n        }\n\n        if ($saved) {\n            $this->finishSave($options);\n        }\n\n        return $saved;\n    }\n\n    /**\n     * Finish processing on a successful save operation.\n     *\n     * @param  array  $options\n     * @return void\n     */\n    protected function finishSave(array $options)\n    {\n        $this->fireModelEvent('saved', false);\n\n        $this->mtime = $this->newQuery()->lastModified();\n\n        $this->syncOriginal();\n    }\n\n    /**\n     * Perform a model update operation.\n     *\n     * @param  October\\Rain\\Halcyon\\Builder  $query\n     * @param  array  $options\n     * @return bool\n     */\n    protected function performUpdate(Builder $query, array $options = [])\n    {\n        $dirty = $this->getDirty();\n\n        if (count($dirty) > 0) {\n            // If the updating event returns false, we will cancel the update operation so\n            // developers can hook Validation systems into their models and cancel this\n            // operation if the model does not pass validation. Otherwise, we update.\n            if ($this->fireModelEvent('updating') === false) {\n                return false;\n            }\n\n            // Recheck dirty attributes as they may have change from the updating event\n            $dirty = $this->getDirty();\n\n            if (count($dirty) > 0) {\n                $query->update($dirty);\n\n                $this->fireModelEvent('updated', false);\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * Perform a model insert operation.\n     *\n     * @param  October\\Rain\\Halcyon\\Builder  $query\n     * @param  array  $options\n     * @return bool\n     */\n    protected function performInsert(Builder $query, array $options = [])\n    {\n        if ($this->fireModelEvent('creating') === false) {\n            return false;\n        }\n\n        // Ensure the settings attribute is passed through so this distinction\n        // is recognized, mainly by the processor.\n        $attributes = $this->attributesToArray();\n\n        $query->insert($attributes);\n\n        // We will go ahead and set the exists property to true, so that it is set when\n        // the created event is fired, just in case the developer tries to update it\n        // during the event. This will allow them to do so and run an update here.\n        $this->exists = true;\n\n        $this->fireModelEvent('created', false);\n\n        return true;\n    }\n\n    /**\n     * Get a new query builder for the object\n     * @return \\October\\Rain\\Halcyon\\Builder\n     */\n    public function newQuery()\n    {\n        $datasource = $this->getDatasource();\n\n        $query = new Builder($datasource, $datasource->getPostProcessor());\n\n        return $query->setModel($this);\n    }\n\n    /**\n     * Create a new Halcyon Collection instance.\n     *\n     * @param  array  $models\n     * @return \\October\\Rain\\Halcyon\\Collection\n     */\n    public function newCollection(array $models = [])\n    {\n        return new Collection($models);\n    }\n\n    /**\n     * getFileNameParts returns the base file name and extension.\n     * Applies a default extension, if none found.\n     */\n    public function getFileNameParts($fileName = null)\n    {\n        if ($fileName === null) {\n            $fileName = $this->fileName;\n        }\n\n        $extension = pathinfo($fileName, PATHINFO_EXTENSION);\n        if (!strlen($extension)) {\n            $extension = $this->defaultExtension;\n            $baseFile = (string) $fileName;\n        }\n        else {\n            $pos = strrpos($fileName, '.');\n            $baseFile = substr($fileName, 0, $pos);\n        }\n\n        return [$baseFile, $extension];\n    }\n\n    /**\n     * getDatasource for the model.\n     *\n     * @return \\October\\Rain\\Halcyon\\Datasource\\DatasourceInterface\n     */\n    public function getDatasource()\n    {\n        return static::resolveDatasource($this->datasource);\n    }\n\n    /**\n     * getDatasourceName for the model.\n     *\n     * @return string\n     */\n    public function getDatasourceName()\n    {\n        return $this->datasource;\n    }\n\n    /**\n     * setDatasource associated with the model.\n     *\n     * @param  string  $name\n     * @return $this\n     */\n    public function setDatasource($name)\n    {\n        $this->datasource = $name;\n\n        return $this;\n    }\n\n    /**\n     * resolveDatasource instance.\n     *\n     * @param  string|null  $datasource\n     * @return \\October\\Rain\\Halcyon\\Datasource\n     */\n    public static function resolveDatasource($datasource = null)\n    {\n        return static::$resolver->datasource($datasource);\n    }\n\n    /**\n     * getDatasourceResolver instance.\n     *\n     * @return \\October\\Rain\\Halcyon\\DatasourceResolverInterface\n     */\n    public static function getDatasourceResolver()\n    {\n        return static::$resolver;\n    }\n\n    /**\n     * setDatasourceResolver instance.\n     *\n     * @param  \\October\\Rain\\Halcyon\\Datasource\\ResolverInterface  $resolver\n     * @return void\n     */\n    public static function setDatasourceResolver(Resolver $resolver)\n    {\n        static::$resolver = $resolver;\n    }\n\n    /**\n     * unsetDatasourceResolver for models.\n     *\n     * @return void\n     */\n    public static function unsetDatasourceResolver()\n    {\n        static::$resolver = null;\n    }\n\n    /**\n     * getCacheManager instance.\n     *\n     * @return \\Illuminate\\Cache\\CacheManager\n     */\n    public static function getCacheManager()\n    {\n        return static::$cache;\n    }\n\n    /**\n     * setCacheManager instance.\n     *\n     * @param  \\Illuminate\\Cache\\CacheManager  $cache\n     * @return void\n     */\n    public static function setCacheManager($cache)\n    {\n        static::$cache = $cache;\n    }\n\n    /**\n     * unsetCacheManager for models.\n     *\n     * @return void\n     */\n    public static function unsetCacheManager()\n    {\n        static::$cache = null;\n    }\n\n    /**\n     * initCacheItem initializes the object properties from the cached data. The extra data\n     * set here becomes available as attributes set on the model after fetch.\n     * @param array $cached The cached data array.\n     */\n    public static function initCacheItem(&$item)\n    {\n    }\n\n    /**\n     * getMutatedAttributes gets the mutated attributes for a given instance.\n     *\n     * @return array\n     */\n    public function getMutatedAttributes()\n    {\n        $class = static::class;\n\n        if (!isset(static::$mutatorCache[$class])) {\n            static::cacheMutatedAttributes($class);\n        }\n\n        return static::$mutatorCache[$class];\n    }\n\n    /**\n     * cacheMutatedAttributes extracts and cache all the mutated attributes of a class.\n     *\n     * @param  string  $class\n     * @return void\n     */\n    public static function cacheMutatedAttributes($class)\n    {\n        $mutatedAttributes = [];\n\n        // Here we will extract all of the mutated attributes so that we can quickly\n        // spin through them after we export models to their array form, which we\n        // need to be fast. This'll let us know the attributes that can mutate.\n        if (preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($class)), $matches)) {\n            foreach ($matches[1] as $match) {\n                $mutatedAttributes[] = lcfirst($match);\n            }\n        }\n\n        static::$mutatorCache[$class] = $mutatedAttributes;\n    }\n\n    /**\n     * __get dynamically retrieve attributes on the model.\n     *\n     * @param  string  $key\n     * @return mixed\n     */\n    public function __get($key)\n    {\n        if ($this->propertyExists($key)) {\n            return $this->extendableGet($key);\n        }\n\n        return $this->getAttribute($key);\n    }\n\n    /**\n     * __set dynamically set attributes on the model.\n     *\n     * @param  string  $key\n     * @param  mixed  $value\n     * @return void\n     */\n    public function __set($key, $value)\n    {\n        if ($this->propertyExists($key)) {\n            $this->extendableSet($key, $value);\n        }\n        else {\n            $this->setAttribute($key, $value);\n        }\n    }\n\n    /**\n     * offsetExists determines if the given attribute exists.\n     *\n     * @param  mixed  $offset\n     * @return bool\n     */\n    public function offsetExists($offset): bool\n    {\n        return isset($this->$offset);\n    }\n\n    /**\n     * offsetGet the value for a given offset.\n     *\n     * @param  mixed  $offset\n     * @return mixed\n     */\n    public function offsetGet($offset): mixed\n    {\n        return $this->$offset;\n    }\n\n    /**\n     * offsetSet the value for a given offset.\n     *\n     * @param  mixed  $offset\n     * @param  mixed  $value\n     * @return void\n     */\n    public function offsetSet($offset, $value): void\n    {\n        $this->$offset = $value;\n    }\n\n    /**\n     * offsetUnset the value for a given offset.\n     *\n     * @param  mixed  $offset\n     * @return void\n     */\n    public function offsetUnset($offset): void\n    {\n        unset($this->$offset);\n    }\n\n    /**\n     * __isset determines if an attribute exists on the model.\n     *\n     * @param  string  $key\n     * @return bool\n     */\n    public function __isset($key)\n    {\n        return isset($this->attributes[$key]) ||\n            (\n                $this->hasGetMutator($key) &&\n                !is_null($this->getAttribute($key))\n            );\n    }\n\n    /**\n     * __unset an attribute on the model.\n     *\n     * @param  string  $key\n     * @return void\n     */\n    public function __unset($key)\n    {\n        unset($this->attributes[$key]);\n    }\n\n    /**\n     * __call handles dynamic method calls into the model.\n     *\n     * @param  string  $method\n     * @param  array  $parameters\n     * @return mixed\n     */\n    public function __call($method, $parameters)\n    {\n        try {\n            return parent::__call($method, $parameters);\n        }\n        catch (BadMethodCallException $ex) {\n            $query = $this->newQuery();\n            return call_user_func_array([$query, $method], $parameters);\n        }\n    }\n\n    /**\n     * __callStatic handles dynamic static method calls into the method.\n     *\n     * @param  string  $method\n     * @param  array  $parameters\n     * @return mixed\n     */\n    public static function __callStatic($method, $parameters)\n    {\n        $instance = new static;\n\n        return call_user_func_array([$instance, $method], $parameters);\n    }\n\n    /**\n     * __toString converts the model to its string representation.\n     *\n     * @return string\n     */\n    public function __toString()\n    {\n        return $this->toJson();\n    }\n\n    /**\n     * __sleep prepare the object for serialization.\n     */\n    public function __sleep()\n    {\n        $this->unbindEvent();\n\n        $this->extendableDestruct();\n\n        return parent::__sleep();\n    }\n\n    /**\n     * __wakeup when a model is being unserialized, check if it needs to be booted.\n     */\n    public function __wakeup()\n    {\n        parent::__wakeup();\n\n        $this->bootIfNotBooted();\n\n        $this->initializeTraits();\n\n        $this->bootNicerEvents();\n\n        $this->initializeModelEvent();\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/Processors/Processor.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Processors;\n\nuse Illuminate\\Support\\Arr;\nuse October\\Rain\\Halcyon\\Builder;\n\n/**\n * Processor\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Processor\n{\n    /**\n     * Process the results of a singular \"select\" query.\n     *\n     * @param  \\October\\Rain\\Halcyon\\Builder  $query\n     * @param  array  $result\n     * @param  string $fileName\n     * @return array\n     */\n    public function processSelectOne(Builder $query, $result)\n    {\n        if ($result === null) {\n            return null;\n        }\n\n        $fileName = Arr::get($result, 'fileName');\n\n        return [$fileName => $this->parseTemplateContent($query, $result, $fileName)];\n    }\n\n    /**\n     * Process the results of a \"select\" query.\n     *\n     * @param  \\October\\Rain\\Halcyon\\Builder  $query\n     * @param  array  $results\n     * @return array\n     */\n    public function processSelect(Builder $query, $results)\n    {\n        if (!count($results)) {\n            return [];\n        }\n\n        $items = [];\n\n        foreach ($results as $result) {\n            $fileName = Arr::get($result, 'fileName');\n            $items[$fileName] = $this->parseTemplateContent($query, $result, $fileName);\n        }\n\n        return $items;\n    }\n\n    /**\n     * Helper to break down template content in to a useful array.\n     * @param  int     $mtime\n     * @param  string  $content\n     * @return array\n     */\n    protected function parseTemplateContent($query, $result, $fileName)\n    {\n        $options = [\n            'isCompoundObject' => $query->getModel()->isCompoundObject()\n        ];\n\n        $content = Arr::get($result, 'content');\n\n        $processed = SectionParser::parse($content, $options);\n\n        return [\n            'fileName' => $fileName,\n            'content' => $content,\n            'mtime' => Arr::get($result, 'mtime'),\n            'markup' => $processed['markup'],\n            'code' => $processed['code']\n        ] + $processed['settings'];\n    }\n\n    /**\n     * Process the data in to an insert action.\n     *\n     * @param  \\October\\Rain\\Halcyon\\Builder  $query\n     * @param  array  $data\n     * @return string\n     */\n    public function processInsert(Builder $query, $data)\n    {\n        $options = [\n            'wrapCodeInPhpTags' => $query->getModel()->getWrapCode(),\n            'isCompoundObject' => $query->getModel()->isCompoundObject()\n        ];\n\n        return SectionParser::render($data, $options);\n    }\n\n    /**\n     * Process the data in to an update action.\n     *\n     * @param  \\October\\Rain\\Halcyon\\Builder  $query\n     * @param  array  $data\n     * @return string\n     */\n    public function processUpdate(Builder $query, $data)\n    {\n        $options = [\n            'wrapCodeInPhpTags' => $query->getModel()->getWrapCode(),\n            'isCompoundObject' => $query->getModel()->isCompoundObject()\n        ];\n\n        $existingData = $query->getModel()->attributesToArray();\n\n        return SectionParser::render($data + $existingData, $options);\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/Processors/SectionParser.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Processors;\n\nuse October\\Rain\\Parse\\Ini;\nuse October\\Rain\\Support\\Str;\n\n/**\n * SectionParser parses CMS object files (pages, partials and layouts).\n * Returns the structured file information.\n *\n * @package october\\halcyon\n * @author Alexey Bobkov, Samuel Georges\n */\nclass SectionParser\n{\n    const SECTION_SEPARATOR = '==';\n\n    const ERROR_INI = '_PARSER_ERROR_INI';\n\n    /**\n     * render a CMS object as file content.\n     * @return string\n     */\n    public static function render($data, $options = [])\n    {\n        extract(array_merge([\n            'wrapCodeInPhpTags' => true,\n            'isCompoundObject' => true\n        ], $options));\n\n        if (!$isCompoundObject) {\n            return $data['content'] ?? '';\n        }\n\n        $iniParser = new Ini;\n        $code = trim($data['code'] ?? '');\n        $markup = trim($data['markup'] ?? '');\n\n        $trim = function (&$values) use (&$trim) {\n            foreach ($values as &$value) {\n                if (!is_array($value)) {\n                    $value = trim($value);\n                }\n                else {\n                    $trim($value);\n                }\n            }\n        };\n\n        $settings = $data['settings'] ?? [];\n        $trim($settings);\n\n        // Build content\n        $content = [];\n\n        // Settings section\n        if ($settings) {\n            $content[] = self::cleanTemplateSection($iniParser->render($settings));\n        }\n\n        // Code section\n        if ($code) {\n            if ($wrapCodeInPhpTags) {\n                $code = preg_replace('/^\\<\\?php/', '', $code);\n                $code = preg_replace('/^\\<\\?/', '', $code);\n                $code = preg_replace('/\\?>$/', '', $code);\n                $code = trim($code, PHP_EOL);\n\n                $content[] = '<?php'.PHP_EOL.$code.PHP_EOL.'?>';\n            }\n            else {\n                $content[] = $code;\n            }\n        }\n\n        // Content section\n        $content[] = self::cleanTemplateSection($markup);\n\n        // Assemble template content\n        $content = trim(implode(PHP_EOL.self::SECTION_SEPARATOR.PHP_EOL, $content));\n\n        return $content;\n    }\n\n    /**\n     * parse a CMS object file content.\n     * The expected file format is following:\n     * <pre>\n     * INI settings section\n     * ==\n     * PHP code section\n     * ==\n     * Twig markup section\n     * </pre>\n     * If the content has only 2 sections they are considered as settings and Twig.\n     * If there is only a single section, it is considered as Twig.\n     *\n     * Returns an array with the following indexes: 'settings', 'markup', 'code'.\n     * The 'markup' and 'code' elements contain strings. The 'settings' element contains the\n     * parsed INI file as array. If the content string doesn't contain a section, the corresponding\n     * result element has null value.\n     * @param string $content\n     * @return array\n     */\n    public static function parse($content, $options = [])\n    {\n        extract(array_merge([\n            'isCompoundObject' => true\n        ], $options));\n\n        $result = [\n            'settings' => [],\n            'code' => null,\n            'markup' => null\n        ];\n\n        if (!$isCompoundObject || !strlen((string) $content)) {\n            return $result;\n        }\n\n        $iniParser = new Ini;\n        $sections = self::splitContentSections($content);\n        $count = count($sections);\n        foreach ($sections as &$section) {\n            $section = trim($section);\n        }\n\n        if ($count >= 3) {\n            $result['settings'] = @$iniParser->parse($sections[0], true)\n                ?: [self::ERROR_INI => $sections[0]];\n\n            $result['code'] = $sections[1];\n            $result['code'] = preg_replace('/^\\s*\\<\\?php/', '', $result['code']);\n            $result['code'] = preg_replace('/^\\s*\\<\\?/', '', $result['code']);\n            $result['code'] = preg_replace('/\\?\\>\\s*$/', '', $result['code']);\n            $result['code'] = trim($result['code'], PHP_EOL);\n\n            $result['markup'] = $sections[2];\n        }\n        elseif ($count === 2) {\n            $result['settings'] = @$iniParser->parse($sections[0], true)\n                ?: [self::ERROR_INI => $sections[0]];\n\n            $result['markup'] = $sections[1];\n        }\n        elseif ($count === 1) {\n            $result['markup'] = $sections[0];\n        }\n\n        return $result;\n    }\n\n    /**\n     * parseOffset is the same as parse method, except using the line number where the\n     * respective section begins is returned. Returns an array with the following indexes:\n     * 'settings', 'markup', 'code'.\n     * @param string $content\n     * @return array\n     */\n    public static function parseOffset($content)\n    {\n        $content = Str::normalizeEol($content);\n        $sections = self::splitContentSections($content);\n        $count = count($sections);\n\n        $result = [\n            'settings' => null,\n            'code'     => null,\n            'markup'   => null\n        ];\n\n        if ($count >= 3) {\n            $result['settings'] = self::adjustLinePosition($content);\n            $result['code'] = self::calculateLinePosition($content);\n            $result['markup'] = self::calculateLinePosition($content, 2);\n        }\n        elseif ($count === 2) {\n            $result['settings'] = self::adjustLinePosition($content);\n            $result['markup'] = self::calculateLinePosition($content);\n        }\n        elseif ($count === 1) {\n            $result['markup'] = 1;\n        }\n\n        return $result;\n    }\n\n    /**\n     * cleanTemplateSection ensures the content does not attempt to escape its section\n     * by using the separator sequence. The content separator is simply removed.\n     */\n    protected static function cleanTemplateSection($content)\n    {\n        return implode('', self::splitContentSections($content));\n    }\n\n    /**\n     * splitContentSections splits a block of content in to sections,\n     * split by the section separator (==).\n     * @param string $content\n     * @return array\n     */\n    protected static function splitContentSections($content)\n    {\n        return preg_split('/^'.preg_quote(self::SECTION_SEPARATOR).'\\s*$/m', $content, -1);\n    }\n\n    /**\n     * calculateLinePosition returns the line number of a found instance of CMS object\n     * section separator (==). Returns the line number the instance was found.\n     * @param string $content\n     * @param int $instance\n     * @return int\n     */\n    protected static function calculateLinePosition($content, $instance = 1)\n    {\n        $count = 0;\n        $lines = explode(PHP_EOL, $content);\n        foreach ($lines as $number => $line) {\n            if (trim($line) === self::SECTION_SEPARATOR) {\n                $count++;\n            }\n\n            if ($count === $instance) {\n                return static::adjustLinePosition($content, $number);\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * adjustLinePosition pushes the starting line number forward since it is not always directly\n     * after the separator (==). There can be an opening tag or white space in between\n     * where the section really begins. The startLine is the calculated starting line\n     * from calculateLinePosition(). Returns the adjusted line number.\n     * @param string $content\n     * @param int $startLine\n     * @return int\n     */\n    protected static function adjustLinePosition($content, $startLine = -1)\n    {\n        // Account for the separator itself\n        $startLine++;\n\n        $lines = array_slice(explode(PHP_EOL, $content), $startLine);\n        foreach ($lines as $line) {\n            $line = trim($line);\n\n            // Empty line\n            if ($line === '') {\n                $startLine++;\n                continue;\n            }\n\n            // PHP line\n            if ($line === '<?php' || $line === '<?') {\n                $startLine++;\n                continue;\n            }\n\n            // PHP namespaced line (use x;) {\n            // Don't increase the line count, it will be rewritten by Cms\\Classes\\CodeParser\n            if (preg_match_all('/(use\\s+[a-z0-9_\\\\\\\\]+;\\n?)/mi', $line) === 1) {\n                continue;\n            }\n\n            break;\n        }\n\n        // Line 0 does not exist\n        return ++$startLine;\n    }\n}\n"
  },
  {
    "path": "src/Halcyon/README.md",
    "content": "## Rain Halcyon\n\nHalcyon is a file based ORM, and the cousin of Eloquent. The goal of this library is to create a solution for file based object storage that shares the same API as database stored models.\n\n### Registering a data source\n\nDatasources reside inside a resolving container called `October\\Rain\\Halcyon\\Datasource\\Resolver`. The following datasources are supported:\n\n- `October\\Rain\\Halcyon\\Datasource\\FileDatasource`: File-based datasource.\n- `October\\Rain\\Halcyon\\Datasource\\DbDatasource`: Database-based datasource.\n- `October\\Rain\\Halcyon\\Datasource\\AutoDatasource`: Loads templates from multiple data sources.\n\nHere is an example of registering a datasource called `theme1`, then binding the resolver to all models.\n\n```php\nuse October\\Rain\\Halcyon\\Model;\nuse October\\Rain\\Filesystem\\Filesystem;\nuse October\\Rain\\Halcyon\\Datasource\\FileDatasource;\nuse October\\Rain\\Halcyon\\Datasource\\Resolver;\n\n$datasource = new FileDatasource('/path/to/theme', new Filesystem);\n$resolver = new Resolver(['theme1' => $datasource]);\n$resolver->setDefaultDatasource('theme1');\nModel::setDatasourceResolver($resolver);\n```\n\n### Model example\n\nInherit the `October\\Rain\\Halcyon\\Model` to create a new model:\n\n```php\n<?php\n\nuse October\\Rain\\Halcyon\\Model;\n\nclass MyPage extends Model\n{\n    /**\n     * @var array The attributes that are mass assignable.\n     */\n    protected $fillable = [\n        'markup',\n        'title',\n    ];\n\n    /**\n     * @var string The container name associated with the model, eg: pages.\n     */\n    protected $dirName = 'pages';\n}\n```\n\nThe following attributes are reserved and have baked in functionality:\n\n- **fileName**: Reserved for the template file name.\n- **content**: Reserved for the complete file contents.\n- **settings**: Stores the template INI settings.\n- **markup**: Stores the template HTML markup.\n- **code**: Stores the template PHP code (optional).\n- **mtime**: Last modified time.\n\nNow we are free to create a new page:\n\n```php\nMyPage::create([\n    'fileName' => 'my-file',\n    'title' => 'Test page',\n    'markup' => '<p>Hello world!</p>'\n]);\n```\n\nExecuting the above code will create a new file **/path/to/theme/pages/my-file.htm**, with the following contents:\n\n```twig\ntitle = \"Test page\"\n==\n<p>Hello world!</p>\n```\n\nWe can find the page and use it later:\n\n```php\n$page = MyPage::find('my-file');\necho '<h1>'.$page->title.'</h1>';\necho $page->markup;\n```\n\nIf we change the file name, it will be renamed on the file system too:\n\n```php\n// New file path: /path/to/theme/pages/renamed-file.htm\n$page->fileName = 'renamed-file';\n$page->save();\n```\n"
  },
  {
    "path": "src/Halcyon/Traits/Validation.php",
    "content": "<?php namespace October\\Rain\\Halcyon\\Traits;\n\nuse Illuminate\\Support\\Arr;\nuse Illuminate\\Support\\MessageBag;\nuse October\\Rain\\Halcyon\\Exception\\ModelException;\nuse October\\Rain\\Support\\Facades\\Input;\nuse Validator;\nuse Exception;\n\ntrait Validation\n{\n    /**\n     * @var array rules to be applied to the data.\n     *\n     * public $rules = [];\n     */\n\n    /**\n     * @var array attributeNames of custom attributes\n     *\n     * public $attributeNames = [];\n     */\n\n    /**\n     * @var array customMessages of custom error messages\n     *\n     * public $customMessages = [];\n     */\n\n    /**\n     * @var bool throwOnValidation makes the validation procedure throw an {@link October\\Rain\\Database\\ModelException}\n     * instead of returning false when validation fails\n     *\n     * public $throwOnValidation = true;\n     */\n\n    /**\n     * @var \\Illuminate\\Support\\MessageBag validationErrors message bag\n     */\n    protected $validationErrors;\n\n    /**\n     * @var \\Illuminate\\Validation\\Validator validator instance\n     */\n    protected static $validator;\n\n    /**\n     * bootValidation trait for this model.\n     */\n    public static function bootValidation()\n    {\n        if (!property_exists(static::class, 'rules')) {\n            throw new Exception(sprintf('You must define a $rules property in %s to use the Validation trait.', static::class));\n        }\n\n        static::extend(function ($model) {\n            $model->bindEvent('model.saveInternal', function ($data, $options) use ($model) {\n                // If forcing the save event, the beforeValidate/afterValidate\n                // events should still fire for consistency. So validate an\n                // empty set of rules and messages.\n                $force = Arr::get($options, 'force', false);\n                if ($force) {\n                    $valid = $model->validate([], []);\n                }\n                else {\n                    $valid = $model->validate();\n                }\n\n                if (!$valid) {\n                    return false;\n                }\n            }, 500);\n        });\n    }\n\n    /**\n     * getValidationAttributes returns the model data used for validation.\n     * @return array\n     */\n    protected function getValidationAttributes()\n    {\n        return $this->getAttributes();\n    }\n\n    /**\n     * makeValidator instantiates the validator used by the validation process, depending if the class is being used inside or\n     * outside of Laravel.\n     * @return \\Illuminate\\Validation\\Validator\n     */\n    protected static function makeValidator($data, $rules, $customMessages, $attributeNames)\n    {\n        return static::getModelValidator()->make($data, $rules, $customMessages, $attributeNames);\n    }\n\n    /**\n     * forceSave the model even if validation fails.\n     * @return bool\n     */\n    public function forceSave($options = null)\n    {\n        return $this->saveInternal(['force' => true] + (array) $options);\n    }\n\n    /**\n     * validate the model instance\n     * @return bool\n     */\n    public function validate($rules = null, $customMessages = null, $attributeNames = null)\n    {\n        if ($this->validationErrors === null) {\n            $this->validationErrors = new MessageBag;\n        }\n\n        $throwOnValidation = property_exists($this, 'throwOnValidation')\n            ? $this->throwOnValidation\n            : true;\n\n        if (($this->fireModelEvent('validating') === false) || ($this->fireEvent('model.beforeValidate') === false)) {\n            if ($throwOnValidation) {\n                throw new ModelException($this);\n            }\n\n            return false;\n        }\n\n        if ($this->methodExists('beforeValidate')) {\n            $this->beforeValidate();\n        }\n\n        // Perform validation\n        $rules = is_null($rules) ? $this->rules : $rules;\n        $rules = $this->processValidationRules($rules);\n        $success = true;\n\n        if (!empty($rules)) {\n            $data = $this->getValidationAttributes();\n\n            $lang = static::getModelValidator()->getTranslator();\n\n            // Custom messages, translate internal references\n            if (property_exists($this, 'customMessages') && is_null($customMessages)) {\n                $customMessages = $this->customMessages;\n            }\n\n            if (is_null($customMessages)) {\n                $customMessages = [];\n            }\n\n            $translatedCustomMessages = [];\n            foreach ($customMessages as $rule => $customMessage) {\n                $translatedCustomMessages[$rule] = $lang->get($customMessage);\n            }\n\n            $customMessages = $translatedCustomMessages;\n\n            // Attribute names, translate internal references\n            if (is_null($attributeNames)) {\n                $attributeNames = [];\n            }\n\n            if (property_exists($this, 'attributeNames')) {\n                $attributeNames = array_merge($this->attributeNames, $attributeNames);\n            }\n\n            $translatedAttributeNames = [];\n            foreach ($attributeNames as $attribute => $attributeName) {\n                $translatedAttributeNames[$attribute] = $lang->get($attributeName);\n            }\n\n            $attributeNames = $translatedAttributeNames;\n\n            // Translate any externally defined attribute names\n            $translations = $lang->get('validation.attributes');\n            if (is_array($translations)) {\n                $attributeNames = array_merge($translations, $attributeNames);\n            }\n\n            // Hand over to the validator\n            $validator = static::makeValidator($data, $rules, $customMessages, $attributeNames);\n\n            $success = $validator->passes();\n\n            if ($success) {\n                if ($this->validationErrors->count() > 0) {\n                    $this->validationErrors = new MessageBag;\n                }\n            }\n            else {\n                $this->validationErrors = $validator->messages();\n\n                // Flash input, if available\n                if (\n                    ($input = Input::getFacadeRoot()) &&\n                    method_exists($input, 'hasSession') &&\n                    $input->hasSession()\n                ) {\n                    $input->flash();\n                }\n            }\n        }\n\n        $this->fireModelEvent('validated', false);\n        $this->fireEvent('model.afterValidate');\n\n        if ($this->methodExists('afterValidate')) {\n            $this->afterValidate();\n        }\n\n        if (!$success && $throwOnValidation) {\n            throw new ModelException($this);\n        }\n\n        return $success;\n    }\n\n    /**\n     * processValidationRules\n     */\n    protected function processValidationRules($rules)\n    {\n        // Run through field names and convert array notation field names to dot notation\n        $rules = $this->processRuleFieldNames($rules);\n\n        foreach ($rules as $field => $ruleParts) {\n            // Trim empty rules\n            if (is_string($ruleParts) && trim($ruleParts) === '') {\n                unset($rules[$field]);\n                continue;\n            }\n\n            // Normalize rulesets\n            if (!is_array($ruleParts)) {\n                $ruleParts = explode('|', $ruleParts);\n            }\n\n            // Analyse each rule individually\n            foreach ($ruleParts as $key => $rulePart) {\n                // Look for required:create and required:update rules\n                if (str_starts_with($rulePart, 'required:create') && $this->exists) {\n                    unset($ruleParts[$key]);\n                }\n                elseif (str_starts_with($rulePart, 'required:update') && !$this->exists) {\n                    unset($ruleParts[$key]);\n                }\n            }\n\n            $rules[$field] = $ruleParts;\n        }\n\n        return $rules;\n    }\n\n    /**\n     * processRuleFieldNames converts any field names using array notation\n     * (ie. `field[child]`) into dot notation (ie. `field.child`)\n     * @param array $rules\n     * @return array\n     */\n    protected function processRuleFieldNames($rules)\n    {\n        $processed = [];\n\n        foreach ($rules as $field => $ruleParts) {\n            $fieldName = $field;\n\n            if (preg_match('/^.*?\\[.*?\\]/', $fieldName)) {\n                $fieldName = str_replace('[]', '.*', $fieldName);\n                $fieldName = str_replace(['[', ']'], ['.', ''], $fieldName);\n            }\n\n            $processed[$fieldName] = $ruleParts;\n        }\n\n        return $processed;\n    }\n\n    /**\n     * isAttributeRequired determines if an attribute is required based on the validation rules.\n     * @param  string  $attribute\n     * @return bool\n     */\n    public function isAttributeRequired($attribute)\n    {\n        if (!isset($this->rules[$attribute])) {\n            return false;\n        }\n\n        $ruleset = $this->rules[$attribute];\n\n        if (is_array($ruleset)) {\n            $ruleset = implode('|', $ruleset);\n        }\n\n        if (strpos($ruleset, 'required:create') !== false && $this->exists) {\n            return false;\n        }\n\n        if (strpos($ruleset, 'required:update') !== false && !$this->exists) {\n            return false;\n        }\n\n        if (strpos($ruleset, 'required_with') !== false) {\n            $requiredWith = substr($ruleset, strpos($ruleset, 'required_with') + 14);\n            $requiredWith = substr($requiredWith, 0, strpos($requiredWith, '|'));\n            return $this->isAttributeRequired($requiredWith);\n        }\n\n        return strpos($ruleset, 'required') !== false;\n    }\n\n    /**\n     * errors gets validation error message collection for the Model\n     * @return \\Illuminate\\Support\\MessageBag\n     */\n    public function errors()\n    {\n        return $this->validationErrors;\n    }\n\n    /**\n     * validating creates a new native event for handling beforeValidate().\n     * @param Closure|string $callback\n     * @return void\n     */\n    public static function validating($callback)\n    {\n        static::registerModelEvent('validating', $callback);\n    }\n\n    /**\n     * validated creates a new native event for handling afterValidate().\n     * @param Closure|string $callback\n     * @return void\n     */\n    public static function validated($callback)\n    {\n        static::registerModelEvent('validated', $callback);\n    }\n\n    /**\n     * getModelValidator instance.\n     * @return \\Illuminate\\Validation\\Validator\n     */\n    public static function getModelValidator()\n    {\n        if (static::$validator === null) {\n            static::$validator = Validator::getFacadeRoot();\n        }\n\n        return static::$validator;\n    }\n\n    /**\n     * setModelValidator instance.\n     * @param  \\Illuminate\\Validation\\Validator\n     * @return void\n     */\n    public static function setModelValidator($validator)\n    {\n        static::$validator = $validator;\n    }\n\n    /**\n     * unsetModelValidator for models.\n     * @return void\n     */\n    public static function unsetModelValidator()\n    {\n        static::$validator = null;\n    }\n}\n"
  },
  {
    "path": "src/Html/BlockBuilder.php",
    "content": "<?php namespace October\\Rain\\Html;\n\nuse Exception;\n\n/**\n * BlockBuilder is used for building placeholders and putting content to them,\n * the content is most often a string, however, it can also store objects.\n *\n * @package october\\html\n * @author Alexey Bobkov, Samuel Georges\n */\nclass BlockBuilder\n{\n    /**\n     * @var array blockStack\n     */\n    protected $blockStack = [];\n\n    /**\n     * @var array blocks\n     */\n    protected $blocks = [];\n\n    /**\n     * put is a helper for startBlock\n     */\n    public function put(string $name)\n    {\n        $this->startBlock($name);\n    }\n\n    /**\n     * startBlock begins the layout block\n     */\n    public function startBlock(string $name)\n    {\n        array_push($this->blockStack, $name);\n        ob_start();\n    }\n\n    /**\n     * endPut is a helper for endBlock and also clears the output buffer\n     * Append indicates that the new content should be appended to the existing block content\n     */\n    public function endPut(bool $append = false)\n    {\n        $this->endBlock($append);\n    }\n\n    /**\n     * endBlock closes the layout block\n     * Append indicates that the new content should be appended to the existing block content\n     */\n    public function endBlock(bool $append = false)\n    {\n        if (!count($this->blockStack)) {\n            throw new Exception('Invalid block nesting');\n        }\n\n        $name = array_pop($this->blockStack);\n        $contents = ob_get_clean();\n\n        if ($append) {\n            $this->append($name, $contents);\n        }\n        else {\n            $this->blocks[$name] = $contents;\n        }\n    }\n\n    /**\n     * set a content of the layout block.\n     */\n    public function set(string $name, $content)\n    {\n        $this->blocks[$name] = $content;\n    }\n\n    /**\n     * append a content of the layout block\n     */\n    public function append(string $name, $content)\n    {\n        if (!isset($this->blocks[$name])) {\n            $this->blocks[$name] = '';\n        }\n\n        $this->blocks[$name] .= $content;\n    }\n\n    /**\n     * placeholder returns the layout block contents and deletes the block from memory.\n     */\n    public function placeholder(string $name, $default = null)\n    {\n        $result = $this->get($name, $default);\n        unset($this->blocks[$name]);\n\n        if (is_string($result)) {\n            $result = trim($result);\n        }\n\n        return $result;\n    }\n\n    /**\n     * has a placeholder set up\n     */\n    public function has(string $name): bool\n    {\n        return isset($this->blocks[$name]);\n    }\n\n    /**\n     * get returns the layout block contents but not deletes the block from memory\n     */\n    public function get(string $name, $default = null)\n    {\n        if (!isset($this->blocks[$name])) {\n            return $default;\n        }\n\n        return $this->blocks[$name];\n    }\n\n    /**\n     * reset clears all the registered blocks\n     */\n    public function reset()\n    {\n        $this->blockStack = [];\n        $this->blocks = [];\n    }\n}\n"
  },
  {
    "path": "src/Html/FormBuilder.php",
    "content": "<?php namespace October\\Rain\\Html;\n\nuse Illuminate\\Support\\Facades\\Request;\nuse Illuminate\\Session\\Store as Session;\nuse Illuminate\\Routing\\UrlGenerator as UrlGeneratorBase;\nuse Illuminate\\Support\\Arr;\n\n/**\n * FormBuilder\n *\n * @package october\\html\n * @author Alexey Bobkov, Samuel Georges\n */\nclass FormBuilder\n{\n    use \\Illuminate\\Support\\Traits\\Macroable;\n\n    /**\n     * @var \\October\\Rain\\Html\\HtmlBuilder html builder instance\n     */\n    protected $html;\n\n    /**\n     * @var \\Illuminate\\Routing\\UrlGenerator url generator instance.\n     */\n    protected $url;\n\n    /**\n     * @var string csrfToken used by the form builder.\n     */\n    protected $csrfToken;\n\n    /**\n     * @var \\Illuminate\\Session\\Store session store implementation.\n     */\n    protected $session;\n\n    /**\n     * @var mixed model instance for the form.\n     */\n    protected $model;\n\n    /**\n     * @var array labels is an array of label names we've created.\n     */\n    protected $labels = [];\n\n    /**\n     * @var array reserved form open attributes.\n     */\n    protected $reserved = [\n        'method',\n        'url',\n        'route',\n        'action',\n        'files',\n        'request',\n        'model',\n        'sessionKey'\n    ];\n\n    /**\n     * @var array reservedAjax form open attributes.\n     */\n    protected $reservedAjax = [\n        'request',\n        'success',\n        'error',\n        'complete',\n        'confirm',\n        'redirect',\n        'update',\n        'data',\n        'validate',\n        'flash',\n        'bulk',\n        'download'\n     ];\n\n    /**\n     * @var array spoofedMethods are form methods that should be spoofed, in uppercase.\n     */\n    protected $spoofedMethods = ['DELETE', 'PATCH', 'PUT'];\n\n    /**\n     * @var array skipValueTypes of inputs to not fill values on by default.\n     */\n    protected $skipValueTypes = ['file', 'password', 'checkbox', 'radio'];\n\n    /**\n     * @var string sessionKey used by the form builder.\n     */\n    protected $sessionKey;\n\n    /**\n     * __construct a new form builder instance.\n     *\n     * @param \\October\\Rain\\Html\\HtmlBuilder  $html\n     * @param \\Illuminate\\Routing\\UrlGenerator  $url\n     * @param string  $csrfToken\n     * @param string  $sessionKey\n     * @return void\n     */\n    public function __construct(HtmlBuilder $html, UrlGeneratorBase $url, $csrfToken, $sessionKey)\n    {\n        $this->url = $url;\n        $this->html = $html;\n        $this->csrfToken = $csrfToken;\n        $this->sessionKey = $sessionKey;\n    }\n\n    /**\n     * open up a new HTML form and includes a session key.\n     * @param array $options\n     * @return string\n     */\n    public function open(array $options = [])\n    {\n        $method = strtoupper(Arr::get($options, 'method', 'post'));\n        $request = Arr::get($options, 'request');\n        $model = Arr::get($options, 'model');\n\n        if ($model) {\n            $this->model = $model;\n        }\n\n        $append = $this->requestHandler($request);\n\n        if ($method !== 'GET') {\n            $append .= $this->sessionKey(Arr::get($options, 'sessionKey'));\n        }\n\n        $attributes = [];\n\n        // We need to extract the proper method from the attributes. If the method is\n        // something other than GET or POST we'll use POST since we will spoof the\n        // actual method since forms don't support the reserved methods in HTML.\n        $attributes['method'] = $this->getMethod($method);\n\n        $attributes['action'] = $this->getAction($options);\n\n        $attributes['accept-charset'] = 'UTF-8';\n\n        // If the method is PUT, PATCH or DELETE we will need to add a spoofer hidden\n        // field that will instruct the Symfony request to pretend the method is a\n        // different method than it actually is, for convenience from the forms.\n        $append .= $this->getAppendage($method);\n\n        if (isset($options['files']) && $options['files']) {\n            $options['enctype'] = 'multipart/form-data';\n        }\n\n        // Finally we're ready to create the final form HTML field. We will attribute\n        // format the array of attributes. We will also add on the appendage which\n        // is used to spoof requests for this PUT, PATCH, etc. methods on forms.\n        $attributes = array_merge(\n            $attributes,\n            Arr::except($options, $this->reserved)\n        );\n\n        // Finally, we will concatenate all of the attributes into a single string so\n        // we can build out the final form open statement. We'll also append on an\n        // extra value for the hidden _method field if it's needed for the form.\n        $attributes = $this->html->attributes($attributes);\n\n        return '<form'.$attributes.'>'.$append;\n    }\n\n    /**\n     * ajax helper for opening a form used for an AJAX call.\n     * @param string $handler Request handler name, eg: onUpdate\n     * @param array $options\n     * @return string\n     */\n    public function ajax($handler, array $options = [])\n    {\n        if (is_array($handler)) {\n            $handler = implode('::', $handler);\n        }\n\n        $attributes = array_merge([\n            'data-request' => $handler\n        ], Arr::except($options, $this->reservedAjax));\n\n        $ajaxAttributes = array_diff_key($options, $attributes);\n        foreach ($ajaxAttributes as $property => $value) {\n            $attributes['data-request-' . $property] = $value;\n        }\n\n        // The `files` option is a hybrid\n        if (isset($options['files'])) {\n            $attributes['data-request-files'] = $options['files'];\n        }\n\n        return $this->open($attributes);\n    }\n\n    /**\n     * model creates a new model based form builder.\n     * @param  mixed  $model\n     * @param  array  $options\n     * @return string\n     */\n    public function model($model, array $options = [])\n    {\n        $this->model = $model;\n\n        return $this->open($options);\n    }\n\n    /**\n     * setModel instance on the form builder.\n     * @param  mixed  $model\n     * @return void\n     */\n    public function setModel($model)\n    {\n        $this->model = $model;\n    }\n\n    /**\n     * close the current form.\n     * @return string\n     */\n    public function close()\n    {\n        $this->labels = [];\n\n        $this->model = null;\n\n        return '</form>';\n    }\n\n    /**\n     * token generates a hidden field with the current CSRF token.\n     * @return string\n     */\n    public function token()\n    {\n        $token = !empty($this->csrfToken)\n            ? $this->csrfToken\n            : $this->session->token();\n\n        return $this->hidden('_token', $token);\n    }\n\n    /**\n     * label creates a form label element.\n     * @param  string  $name\n     * @param  string  $value\n     * @param  array   $options\n     * @return string\n     */\n    public function label($name, $value = null, $options = [])\n    {\n        $this->labels[] = $name;\n\n        $options = $this->html->attributes($options);\n\n        $value = e($this->formatLabel($name, $value));\n\n        return '<label for=\"'.$name.'\"'.$options.'>'.$value.'</label>';\n    }\n\n    /**\n     * formatLabel value.\n     * @param  string  $name\n     * @param  string|null  $value\n     * @return string\n     */\n    protected function formatLabel($name, $value)\n    {\n        return $value ?: ucwords(str_replace('_', ' ', $name));\n    }\n\n    /**\n     * input creates a form input field.\n     * @param  string  $type\n     * @param  string  $name\n     * @param  string  $value\n     * @param  array   $options\n     * @return string\n     */\n    public function input($type, $name, $value = null, $options = [])\n    {\n        if (!isset($options['name'])) {\n            $options['name'] = $name;\n        }\n\n        // We will get the appropriate value for the given field. We will look for the\n        // value in the session for the value in the old input data then we'll look\n        // in the model instance if one is set. Otherwise we will just use empty.\n        $id = $this->getIdAttribute($name, $options);\n\n        if (!in_array($type, $this->skipValueTypes)) {\n            $value = $this->getValueAttribute($name, $value);\n        }\n\n        // Once we have the type, value, and ID we can merge them into the rest of the\n        // attributes array so we can convert them into their HTML attribute format\n        // when creating the HTML element. Then, we will return the entire input.\n        $merge = compact('type', 'value', 'id');\n\n        $options = array_merge($options, $merge);\n\n        return '<input'.$this->html->attributes($options).'>';\n    }\n\n    /**\n     * text input field.\n     * @param  string  $name\n     * @param  string  $value\n     * @param  array   $options\n     * @return string\n     */\n    public function text($name, $value = null, $options = [])\n    {\n        return $this->input('text', $name, $value, $options);\n    }\n\n    /**\n     * password input field.\n     * @param  string  $name\n     * @param  array   $options\n     * @return string\n     */\n    public function password($name, $options = [])\n    {\n        return $this->input('password', $name, '', $options);\n    }\n\n    /**\n     * hidden input field.\n     * @param  string  $name\n     * @param  string  $value\n     * @param  array   $options\n     * @return string\n     */\n    public function hidden($name, $value = null, $options = [])\n    {\n        return $this->input('hidden', $name, $value, $options);\n    }\n\n    /**\n     * email input field.\n     * @param  string  $name\n     * @param  string  $value\n     * @param  array   $options\n     * @return string\n     */\n    public function email($name, $value = null, $options = [])\n    {\n        return $this->input('email', $name, $value, $options);\n    }\n\n    /**\n     * number input field.\n     * @param  string  $name\n     * @param  string  $value\n     * @param  array   $options\n     * @return string\n     */\n    public function number($name, $value = null, $options = [])\n    {\n        return $this->input('number', $name, $value, $options);\n    }\n\n    /**\n     * url input field.\n     * @param  string  $name\n     * @param  string  $value\n     * @param  array   $options\n     * @return string\n     */\n    public function url($name, $value = null, $options = [])\n    {\n        return $this->input('url', $name, $value, $options);\n    }\n\n    /**\n     * file input field.\n     * @param  string  $name\n     * @param  array   $options\n     * @return string\n     */\n    public function file($name, $options = [])\n    {\n        return $this->input('file', $name, null, $options);\n    }\n\n    //\n    // Textarea\n    //\n\n    /**\n     * textarea input field.\n     * @param  string  $name\n     * @param  string  $value\n     * @param  array   $options\n     * @return string\n     */\n    public function textarea($name, $value = null, $options = [])\n    {\n        if (!isset($options['name'])) {\n            $options['name'] = $name;\n        }\n\n        // Next we will look for the rows and cols attributes, as each of these are put\n        // on the textarea element definition. If they are not present, we will just\n        // assume some sane default values for these attributes for the developer.\n        $options = $this->setTextAreaSize($options);\n\n        $options['id'] = $this->getIdAttribute($name, $options);\n\n        $value = (string) $this->getValueAttribute($name, $value);\n\n        unset($options['size']);\n\n        // Next we will convert the attributes into a string form. Also we have removed\n        // the size attribute, as it was merely a short-cut for the rows and cols on\n        // the element. Then we'll create the final textarea elements HTML for us.\n        $options = $this->html->attributes($options);\n\n        return '<textarea'.$options.'>'.e($value).'</textarea>';\n    }\n\n    /**\n     * setTextAreaSize on the attributes.\n     * @param  array  $options\n     * @return array\n     */\n    protected function setTextAreaSize($options)\n    {\n        if (isset($options['size'])) {\n            return $this->setQuickTextAreaSize($options);\n        }\n\n        // If the \"size\" attribute was not specified, we will just look for the regular\n        // columns and rows attributes, using sane defaults if these do not exist on\n        // the attributes array. We'll then return this entire options array back.\n        $cols = Arr::get($options, 'cols', 50);\n\n        $rows = Arr::get($options, 'rows', 10);\n\n        return array_merge($options, compact('cols', 'rows'));\n    }\n\n    /**\n     * setQuickTextAreaSize using the quick \"size\" attribute.\n     *\n     * @param  array  $options\n     * @return array\n     */\n    protected function setQuickTextAreaSize($options)\n    {\n        $segments = explode('x', $options['size']);\n\n        return array_merge($options, ['cols' => $segments[0], 'rows' => $segments[1]]);\n    }\n\n    //\n    // Select\n    //\n\n    /**\n     * select box field with empty option support.\n     * @param  string  $name\n     * @param  array   $list\n     * @param  string  $selected\n     * @param  array   $options\n     * @return string\n     */\n    public function select($name, $list = [], $selected = null, $options = [])\n    {\n        if (array_key_exists('emptyOption', $options)) {\n            $list = ['' => $options['emptyOption']] + $list;\n        }\n\n        $selectOptions = false;\n        if (array_key_exists('selectOptions', $options)) {\n            $selectOptions = $options['selectOptions'] === true;\n            unset($options['selectOptions']);\n        }\n\n        // When building a select box the \"value\" attribute is really the selected one\n        // so we will use that when checking the model or session for a value which\n        // should provide a convenient method of re-populating the forms on post.\n        $selected = $this->getValueAttribute($name, $selected);\n\n        $options['id'] = $this->getIdAttribute($name, $options);\n\n        if (!isset($options['name'])) {\n            $options['name'] = $name;\n        }\n\n        // We will simply loop through the options and build an HTML value for each of\n        // them until we have an array of HTML declarations. Then we will join them\n        // all together into one single HTML element that can be put on the form.\n        $html = [];\n\n        foreach ($list as $value => $display) {\n            $html[] = $this->getSelectOption($display, $value, $selected);\n        }\n\n        // Once we have all of this HTML, we can join this into a single element after\n        // formatting the attributes into an HTML \"attributes\" string, then we will\n        // build out a final select statement, which will contain all the values.\n        $options = $this->html->attributes($options);\n\n        $list = implode('', $html);\n\n        return $selectOptions ? $list : \"<select{$options}>{$list}</select>\";\n    }\n\n    /**\n     * selectOptions only renders the options inside a select.\n     * @param  string  $name\n     * @param  array   $list\n     * @param  string  $selected\n     * @param  array   $options\n     * @return string\n     */\n    public function selectOptions($name, $list = [], $selected = null, $options = [])\n    {\n        return $this->select($name, $list, $selected, ['selectOptions' => true] + $options);\n    }\n\n    /**\n     * selectRange field.\n     * @param  string  $name\n     * @param  string  $begin\n     * @param  string  $end\n     * @param  string  $selected\n     * @param  array   $options\n     * @return string\n     */\n    public function selectRange($name, $begin, $end, $selected = null, $options = [])\n    {\n        $range = array_combine($range = range($begin, $end), $range);\n\n        return $this->select($name, $range, $selected, $options);\n    }\n\n    /**\n     * selectYear field.\n     * @param  string  $name\n     * @param  string  $begin\n     * @param  string  $end\n     * @param  string  $selected\n     * @param  array   $options\n     * @return string\n     */\n    public function selectYear()\n    {\n        return call_user_func_array([$this, 'selectRange'], func_get_args());\n    }\n\n    /**\n     * selectMonth field.\n     * @param  string  $name\n     * @param  string  $selected\n     * @param  array   $options\n     * @param  string  $format DateTime format string (default: 'F' for full month name)\n     * @return string\n     */\n    public function selectMonth($name, $selected = null, $options = [], $format = 'F')\n    {\n        $months = [];\n\n        foreach (range(1, 12) as $month) {\n            $date = new \\DateTime(\"2024-{$month}-01\");\n            $months[$month] = $date->format($format);\n        }\n\n        return $this->select($name, $months, $selected, $options);\n    }\n\n    /**\n     * getSelectOption for the given value.\n     * @param  string  $display\n     * @param  string  $value\n     * @param  string  $selected\n     * @return string\n     */\n    public function getSelectOption($display, $value, $selected)\n    {\n        if (is_array($display)) {\n            return $this->optionGroup($display, $value, $selected);\n        }\n\n        return $this->option($display, $value, $selected);\n    }\n\n    /**\n     * optionGroup form element.\n     * @param  array   $list\n     * @param  string  $label\n     * @param  string  $selected\n     * @return string\n     */\n    protected function optionGroup($list, $label, $selected)\n    {\n        $html = [];\n\n        foreach ($list as $value => $display) {\n            $html[] = $this->option($display, $value, $selected);\n        }\n\n        return '<optgroup label=\"'.e($label).'\">'.implode('', $html).'</optgroup>';\n    }\n\n    /**\n     * option for a select element option.\n     * @param  string  $display\n     * @param  string  $value\n     * @param  string  $selected\n     * @return string\n     */\n    protected function option($display, $value, $selected)\n    {\n        $selected = $this->getSelectedValue($value, $selected);\n\n        $options = ['value' => e($value), 'selected' => $selected];\n\n        return '<option'.$this->html->attributes($options).'>'.e($display).'</option>';\n    }\n\n    /**\n     * getSelectedValue determines if the value is selected.\n     * @param  string  $value\n     * @param  string  $selected\n     * @return string\n     */\n    protected function getSelectedValue($value, $selected)\n    {\n        if (is_array($selected)) {\n            return in_array($value, $selected) ? 'selected' : null;\n        }\n\n        return ((string) $value === (string) $selected) ? 'selected' : null;\n    }\n\n    //\n    // Checkbox\n    //\n\n    /**\n     * checkbox input field.\n     * @param  string  $name\n     * @param  mixed   $value\n     * @param  bool    $checked\n     * @param  array   $options\n     * @return string\n     */\n    public function checkbox($name, $value = 1, $checked = null, $options = [])\n    {\n        return $this->checkable('checkbox', $name, $value, $checked, $options);\n    }\n\n    /**\n     * radio button input field.\n     * @param  string  $name\n     * @param  mixed   $value\n     * @param  bool    $checked\n     * @param  array   $options\n     * @return string\n     */\n    public function radio($name, $value = null, $checked = null, $options = [])\n    {\n        if (is_null($value)) {\n            $value = $name;\n        }\n\n        return $this->checkable('radio', $name, $value, $checked, $options);\n    }\n\n    /**\n     * checkable input field.\n     * @param  string  $type\n     * @param  string  $name\n     * @param  mixed   $value\n     * @param  bool    $checked\n     * @param  array   $options\n     * @return string\n     */\n    protected function checkable($type, $name, $value, $checked, $options)\n    {\n        $checked = $this->getCheckedState($type, $name, $value, $checked);\n\n        if ($checked) {\n            $options['checked'] = 'checked';\n        }\n\n        return $this->input($type, $name, $value, $options);\n    }\n\n    /**\n     * getCheckedState for a checkable input.\n     * @param  string  $type\n     * @param  string  $name\n     * @param  mixed   $value\n     * @param  bool    $checked\n     * @return bool\n     */\n    protected function getCheckedState($type, $name, $value, $checked)\n    {\n        switch ($type) {\n            case 'checkbox':\n                return $this->getCheckboxCheckedState($name, $value, $checked);\n\n            case 'radio':\n                return $this->getRadioCheckedState($name, $value, $checked);\n\n            default:\n                return $this->getValueAttribute($name) === $value;\n        }\n    }\n\n    /**\n     * getCheckboxCheckedState for a checkbox input.\n     * @param  string  $name\n     * @param  mixed  $value\n     * @param  bool  $checked\n     * @return bool\n     */\n    protected function getCheckboxCheckedState($name, $value, $checked)\n    {\n        if (\n            isset($this->session) &&\n            !$this->oldInputIsEmpty() &&\n            is_null($this->old($name))\n        ) {\n            return false;\n        }\n\n        if ($this->missingOldAndModel($name)) {\n            return $checked;\n        }\n\n        $posted = $this->getValueAttribute($name);\n\n        return is_array($posted) ? in_array($value, $posted) : (bool) $posted;\n    }\n\n    /**\n     * getRadioCheckedState for a radio input.\n     * @param  string  $name\n     * @param  mixed  $value\n     * @param  bool  $checked\n     * @return bool\n     */\n    protected function getRadioCheckedState($name, $value, $checked)\n    {\n        if ($this->missingOldAndModel($name)) {\n            return $checked;\n        }\n\n        return $this->getValueAttribute($name) === $value;\n    }\n\n    /**\n     * missingOldAndModel determines if old input or model input exists for a key.\n     * @param  string  $name\n     * @return bool\n     */\n    protected function missingOldAndModel($name)\n    {\n        return (is_null($this->old($name)) && is_null($this->getModelValueAttribute($name)));\n    }\n\n    /**\n     * reset input element.\n     * @param  string  $value\n     * @param  array   $attributes\n     * @return string\n     */\n    public function reset($value, $attributes = [])\n    {\n        return $this->input('reset', null, $value, $attributes);\n    }\n\n    /**\n     * image input element.\n     * @param  string  $url\n     * @param  string  $name\n     * @param  array   $attributes\n     * @return string\n     */\n    public function image($url, $name = null, $attributes = [])\n    {\n        $attributes['src'] = $this->url->asset($url);\n\n        return $this->input('image', $name, null, $attributes);\n    }\n\n    /**\n     * submit button element.\n     * @param  string  $value\n     * @param  array   $options\n     * @return string\n     */\n    public function submit($value = null, $options = [])\n    {\n        return $this->input('submit', null, $value, $options);\n    }\n\n    /**\n     * button element.\n     * @param  string  $value\n     * @param  array   $options\n     * @return string\n     */\n    public function button($value = null, $options = [])\n    {\n        if (!array_key_exists('type', $options)) {\n            $options['type'] = 'button';\n        }\n\n        return '<button'.$this->html->attributes($options).'>'.$value.'</button>';\n    }\n\n    /**\n     * getMethod parses the form action method.\n     * @param  string  $method\n     * @return string\n     */\n    protected function getMethod($method)\n    {\n        $method = strtoupper($method);\n\n        return $method !== 'GET' ? 'POST' : $method;\n    }\n\n    /**\n     * getAction gets the form action from the options.\n     * @param  array   $options\n     * @return string\n     */\n    protected function getAction(array $options)\n    {\n        // We will also check for a \"route\" or \"action\" parameter on the array so that\n        // developers can easily specify a route or controller action when creating\n        // a form providing a convenient interface for creating the form actions.\n        if (isset($options['url'])) {\n            return $this->getUrlAction($options['url']);\n        }\n\n        if (isset($options['route'])) {\n            return $this->getRouteAction($options['route']);\n        }\n\n        // If an action is available, we are attempting to open a form to a controller\n        // action route. So, we will use the URL generator to get the path to these\n        // actions and return them from the method. Otherwise, we'll use current.\n        elseif (isset($options['action'])) {\n            return $this->getControllerAction($options['action']);\n        }\n\n        return $this->url->current();\n    }\n\n    /**\n     * getUrlAction gets the action for a \"url\" option.\n     * @param  array|string  $options\n     * @return string\n     */\n    protected function getUrlAction($options)\n    {\n        if (is_array($options)) {\n            return $this->url->to($options[0], array_slice($options, 1));\n        }\n\n        return $this->url->to($options);\n    }\n\n    /**\n     * getRouteAction gets the action for a \"route\" option.\n     * @param  array|string  $options\n     * @return string\n     */\n    protected function getRouteAction($options)\n    {\n        if (is_array($options)) {\n            return $this->url->route($options[0], array_slice($options, 1));\n        }\n\n        return $this->url->route($options);\n    }\n\n    /**\n     * getControllerAction gets the action for an \"action\" option.\n     * @param  array|string  $options\n     * @return string\n     */\n    protected function getControllerAction($options)\n    {\n        if (is_array($options)) {\n            return $this->url->action($options[0], array_slice($options, 1));\n        }\n\n        return $this->url->action($options);\n    }\n\n    /**\n     * getAppendage gets the form appendage for the given method.\n     * @param  string  $method\n     * @return string\n     */\n    protected function getAppendage($method)\n    {\n        list($method, $appendage) = [strtoupper($method), ''];\n\n        // If the HTTP method is in this list of spoofed methods, we will attach the\n        // method spoofer hidden input to the form. This allows us to use regular\n        // form to initiate PUT and DELETE requests in addition to the typical.\n        if (in_array($method, $this->spoofedMethods)) {\n            $appendage .= $this->hidden('_method', $method);\n        }\n\n        // If the method is something other than GET we will go ahead and attach the\n        // CSRF token to the form, as this can't hurt and is convenient to simply\n        // always have available on every form the developers creates for them.\n        if ($method !== 'GET') {\n            $appendage .= $this->token();\n        }\n\n        return $appendage;\n    }\n\n    /**\n     * getIdAttribute for a field name.\n     * @param  string  $name\n     * @param  array   $attributes\n     * @return string\n     */\n    public function getIdAttribute($name, $attributes)\n    {\n        if (array_key_exists('id', $attributes)) {\n            return $attributes['id'];\n        }\n\n        if (in_array($name, $this->labels)) {\n            return $name;\n        }\n    }\n\n    /**\n     * getValueAttribute that should be assigned to the field.\n     * @param  string  $name\n     * @param  string  $value\n     * @return string\n     */\n    public function getValueAttribute($name, $value = null)\n    {\n        if (is_null($name)) {\n            return $value;\n        }\n\n        if (!is_null($this->old($name))) {\n            return $this->old($name);\n        }\n\n        if (!is_null($value)) {\n            return $value;\n        }\n\n        if (isset($this->model)) {\n            return $this->getModelValueAttribute($name);\n        }\n    }\n\n    /**\n     * getModelValueAttribute that should be assigned to the field.\n     * @param  string  $name\n     * @return string\n     */\n    protected function getModelValueAttribute($name)\n    {\n        if (is_object($this->model)) {\n            return object_get($this->model, $this->transformKey($name));\n        }\n        elseif (is_array($this->model)) {\n            return Arr::get($this->model, $this->transformKey($name));\n        }\n    }\n\n    /**\n     * old gets a value from the session's old input.\n     * @param  string  $name\n     * @return string\n     */\n    public function old($name)\n    {\n        if (isset($this->session)) {\n            return $this->session->getOldInput($this->transformKey($name));\n        }\n    }\n\n    /**\n     * oldInputIsEmpty determines if the old input is empty.\n     * @return bool\n     */\n    public function oldInputIsEmpty()\n    {\n        return (isset($this->session) && count($this->session->getOldInput()) === 0);\n    }\n\n    /**\n     * transformKey from array to dot syntax.\n     * @param  string  $key\n     * @return string\n     */\n    protected function transformKey($key)\n    {\n        return str_replace(['.', '[]', '[', ']'], ['_', '', '.', ''], $key);\n    }\n\n    /**\n     * getSessionStore implementation.\n     * @return  \\Illuminate\\Session\\Store  $session\n     */\n    public function getSessionStore()\n    {\n        return $this->session;\n    }\n\n    /**\n     * setSessionStore implementation.\n     * @param  \\Illuminate\\Session\\Store  $session\n     * @return $this\n     */\n    public function setSessionStore(Session $session)\n    {\n        $this->session = $session;\n\n        return $this;\n    }\n\n    /**\n     * value is a helper for getting form values. Tries to find the old value,\n     * then uses a postback/get value, then looks at the form model values.\n     * @param  string $name\n     * @param  string $value\n     * @return string\n     */\n    public function value($name, $value = null)\n    {\n        if (is_null($name)) {\n            return $value;\n        }\n\n        if (!is_null($this->old($name))) {\n            return $this->old($name);\n        }\n\n        $inputValue = Request::input(Helper::nameToDot($name));\n        if (!is_null($inputValue)) {\n            return $inputValue;\n        }\n\n        if (isset($this->model)) {\n            return $this->getModelValueAttribute($name);\n        }\n\n        return $value;\n    }\n\n    /**\n     * requestHandler returns a hidden HTML input, supplying the postback request handler.\n     * @return string\n     */\n    protected function requestHandler($name = null)\n    {\n        if (!$name) {\n            return '';\n        }\n\n        return $this->hidden('_handler', $name);\n    }\n\n    /**\n     * sessionKey returns a hidden HTML input, supplying the session key value.\n     * @return string\n     */\n    public function sessionKey($sessionKey = null)\n    {\n        if (!$sessionKey) {\n            $sessionKey = Request::post('_session_key', $this->sessionKey);\n        }\n\n        return $this->hidden('_session_key', $sessionKey);\n    }\n\n    /**\n     * getSessionKey returns the active session key, used fr deferred bindings.\n     * @return string\n     */\n    public function getSessionKey()\n    {\n        return $this->sessionKey;\n    }\n}\n"
  },
  {
    "path": "src/Html/Helper.php",
    "content": "<?php namespace October\\Rain\\Html;\n\n/**\n * Helper methods that may be useful for processing HTML tasks\n *\n * @package october\\html\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Helper\n{\n    /**\n     * nameToId converts a HTML array string to an identifier string.\n     * HTML: user[location][city]\n     * Result: user-location-city\n     * @param $string String to process\n     * @return string\n     */\n    public static function nameToId($string)\n    {\n        return rtrim(str_replace('--', '-', str_replace(['[', ']', ':', '.'], '-', $string)), '-');\n    }\n\n    /**\n     * nameToArray converts a HTML named array string to a PHP array. Empty values are removed.\n     * HTML: user[location][city]\n     * PHP:  ['user', 'location', 'city']\n     * @param $string\n     * @return array\n     */\n    public static function nameToArray($string)\n    {\n        $result = [$string];\n\n        if (strpbrk($string, '[]') === false) {\n            return $result;\n        }\n\n        if (preg_match('/^([^\\]]+)(?:\\[(.+)\\])+$/', $string, $matches)) {\n            if (count($matches) < 2) {\n                return $result;\n            }\n\n            $result = explode('][', $matches[2]);\n            array_unshift($result, $matches[1]);\n        }\n\n        $result = array_filter($result, function ($val) {\n            return strlen($val);\n        });\n\n        return $result;\n    }\n\n    /**\n     * nameToDot converts a HTML named array string to a dot notated string.\n     * HTML: user[location][city]\n     * Dot: user.location.city\n     * @param $string\n     * @return string\n     */\n    public static function nameToDot($string)\n    {\n        return implode('.', static::nameToArray($string));\n    }\n\n    /**\n     * reduceNameHierarchy reduces the field name hierarchy depth by $level levels.\n     * country[city][0][street][0] turns into country[city][0] when reduced by 1 level;\n     * country[city][0][street][0] turns into country when reduced by 2 levels;\n     * etc.\n     *\n     * @param string $fieldName\n     * @param int $level\n     * @return string\n     */\n    public static function reduceNameHierarchy($fieldName, $level)\n    {\n        $formName = self::nameToArray($fieldName);\n        $sliceLength = count($formName) - $level * 2;\n\n        if ($sliceLength <= 1) {\n            return $formName[0];\n        }\n\n        $formName = array_slice($formName, 0, $sliceLength);\n        $formNameFirst = array_shift($formName);\n\n        return $formNameFirst.'['.implode('][', $formName).']';\n    }\n}\n"
  },
  {
    "path": "src/Html/HtmlBuilder.php",
    "content": "<?php namespace October\\Rain\\Html;\n\nuse October\\Rain\\Support\\Str;\nuse Illuminate\\Routing\\UrlGenerator;\nuse Symfony\\Component\\HtmlSanitizer\\HtmlSanitizer;\nuse Symfony\\Component\\HtmlSanitizer\\HtmlSanitizerConfig;\nuse enshrined\\svgSanitize\\Sanitizer as SvgSanitizer;\n\n/**\n * HtmlBuilder builds HTML elements\n *\n * @package october\\html\n * @author Alexey Bobkov, Samuel Georges\n */\nclass HtmlBuilder\n{\n    use \\Illuminate\\Support\\Traits\\Macroable;\n\n    /**\n     * url generator instance.\n     *\n     * @var \\Illuminate\\Routing\\UrlGenerator\n     */\n    protected $url;\n\n    /**\n     * __construct a new HTML builder instance.\n     *\n     * @param  \\Illuminate\\Routing\\UrlGenerator  $url\n     * @return void\n     */\n    public function __construct(?UrlGenerator $url = null)\n    {\n        $this->url = $url;\n    }\n\n    /**\n     * entities converts an HTML string to entities.\n     *\n     * @param  string  $value\n     * @return string\n     */\n    public function entities($value)\n    {\n        return htmlentities($value, ENT_QUOTES, 'UTF-8', false);\n    }\n\n    /**\n     * decode converts entities to HTML characters.\n     *\n     * @param  string  $value\n     * @return string\n     */\n    public function decode($value)\n    {\n        return html_entity_decode($value, ENT_QUOTES, 'UTF-8');\n    }\n\n    /**\n     * script generates a link to a JavaScript file.\n     *\n     * @param  string  $url\n     * @param  array   $attributes\n     * @param  bool    $secure\n     * @return string\n     */\n    public function script($url, $attributes = [], $secure = null)\n    {\n        $attributes['src'] = $this->url->asset($url, $secure);\n\n        return '<script'.$this->attributes($attributes).'></script>'.PHP_EOL;\n    }\n\n    /**\n     * style generates a link to a CSS file.\n     *\n     * @param  string  $url\n     * @param  array   $attributes\n     * @param  bool    $secure\n     * @return string\n     */\n    public function style($url, $attributes = [], $secure = null)\n    {\n        $defaults = ['media' => 'all', 'type' => 'text/css', 'rel' => 'stylesheet'];\n\n        $attributes = $attributes + $defaults;\n\n        $attributes['href'] = $this->url->asset($url, $secure);\n\n        return '<link'.$this->attributes($attributes).'>'.PHP_EOL;\n    }\n\n    /**\n     * image generates an HTML image element.\n     *\n     * @param  string  $url\n     * @param  string  $alt\n     * @param  array   $attributes\n     * @param  bool    $secure\n     * @return string\n     */\n    public function image($url, $alt = null, $attributes = [], $secure = null)\n    {\n        $attributes['alt'] = $alt;\n\n        return '<img src=\"'.$this->url->asset($url, $secure).'\"'.$this->attributes($attributes).'>';\n    }\n\n    /**\n     * link generates a HTML link.\n     *\n     * @param  string  $url\n     * @param  string  $title\n     * @param  array   $attributes\n     * @param  bool    $secure\n     * @return string\n     */\n    public function link($url, $title = null, $attributes = [], $secure = null)\n    {\n        $url = $this->url->to($url, [], $secure);\n\n        if (is_null($title) || $title === false) {\n            $title = $url;\n        }\n\n        return '<a href=\"'.$url.'\"'.$this->attributes($attributes).'>'.$this->entities($title).'</a>';\n    }\n\n    /**\n     * secureLink generates a HTTPS HTML link.\n     *\n     * @param  string  $url\n     * @param  string  $title\n     * @param  array   $attributes\n     * @return string\n     */\n    public function secureLink($url, $title = null, $attributes = [])\n    {\n        return $this->link($url, $title, $attributes, true);\n    }\n\n    /**\n     * linkAsset generates a HTML link to an asset.\n     *\n     * @param  string  $url\n     * @param  string  $title\n     * @param  array   $attributes\n     * @param  bool    $secure\n     * @return string\n     */\n    public function linkAsset($url, $title = null, $attributes = [], $secure = null)\n    {\n        $url = $this->url->asset($url, $secure);\n\n        return $this->link($url, $title ?: $url, $attributes, $secure);\n    }\n\n    /**\n     * linkSecureAsset generates a HTTPS HTML link to an asset.\n     *\n     * @param  string  $url\n     * @param  string  $title\n     * @param  array   $attributes\n     * @return string\n     */\n    public function linkSecureAsset($url, $title = null, $attributes = [])\n    {\n        return $this->linkAsset($url, $title, $attributes, true);\n    }\n\n    /**\n     * linkRoute generates a HTML link to a named route.\n     *\n     * @param  string  $name\n     * @param  string  $title\n     * @param  array   $parameters\n     * @param  array   $attributes\n     * @return string\n     */\n    public function linkRoute($name, $title = null, $parameters = [], $attributes = [])\n    {\n        return $this->link($this->url->route($name, $parameters), $title, $attributes);\n    }\n\n    /**\n     * linkAction generates a HTML link to a controller action.\n     *\n     * @param  string  $action\n     * @param  string  $title\n     * @param  array   $parameters\n     * @param  array   $attributes\n     * @return string\n     */\n    public function linkAction($action, $title = null, $parameters = [], $attributes = [])\n    {\n        return $this->link($this->url->action($action, $parameters), $title, $attributes);\n    }\n\n    /**\n     * mailto generates a HTML link to an email address.\n     *\n     * @param  string  $email\n     * @param  string  $title\n     * @param  array   $attributes\n     * @return string\n     */\n    public function mailto($email, $title = null, $attributes = [])\n    {\n        $email = $this->email($email);\n\n        $title = $title ?: $email;\n\n        $email = $this->obfuscate('mailto:') . $email;\n\n        return '<a href=\"'.$email.'\"'.$this->attributes($attributes).'>'.$this->entities($title).'</a>';\n    }\n\n    /**\n     * email obfuscates an e-mail address to prevent spam-bots from sniffing it.\n     *\n     * @param  string  $email\n     * @return string\n     */\n    public function email($email)\n    {\n        return str_replace('@', '&#64;', $this->obfuscate($email));\n    }\n\n    /**\n     * ol generate an ordered list of items.\n     *\n     * @param  array   $list\n     * @param  array   $attributes\n     * @return string\n     */\n    public function ol($list, $attributes = [])\n    {\n        return $this->listing('ol', $list, $attributes);\n    }\n\n    /**\n     * ul generates an un-ordered list of items.\n     *\n     * @param  array   $list\n     * @param  array   $attributes\n     * @return string\n     */\n    public function ul($list, $attributes = [])\n    {\n        return $this->listing('ul', $list, $attributes);\n    }\n\n    /**\n     * listing HTML element.\n     *\n     * @param  string  $type\n     * @param  array   $list\n     * @param  array   $attributes\n     * @return string\n     */\n    protected function listing($type, $list, $attributes = [])\n    {\n        $html = '';\n\n        if (count($list) === 0) {\n            return $html;\n        }\n\n        // Essentially we will just spin through the list and build the list of the HTML\n        // elements from the array. We will also handled nested lists in case that is\n        // present in the array. Then we will build out the final listing elements.\n        foreach ($list as $key => $value) {\n            $html .= $this->listingElement($key, $type, $value);\n        }\n\n        $attributes = $this->attributes($attributes);\n\n        return \"<{$type}{$attributes}>{$html}</{$type}>\";\n    }\n\n    /**\n     * listingElement creates the HTML for a listing element.\n     *\n     * @param  mixed    $key\n     * @param  string  $type\n     * @param  string  $value\n     * @return string\n     */\n    protected function listingElement($key, $type, $value)\n    {\n        if (is_array($value)) {\n            return $this->nestedListing($key, $type, $value);\n        }\n\n        return '<li>'.e($value).'</li>';\n    }\n\n    /**\n     * nestedListing creates the HTML for a nested listing attribute.\n     *\n     * @param  mixed    $key\n     * @param  string  $type\n     * @param  string  $value\n     * @return string\n     */\n    protected function nestedListing($key, $type, $value)\n    {\n        if (is_int($key)) {\n            return $this->listing($type, $value);\n        }\n\n        return '<li>'.$key.$this->listing($type, $value).'</li>';\n    }\n\n    /**\n     * Build an HTML attribute string from an array.\n     *\n     * @param  array  $attributes\n     * @return string\n     */\n    public function attributes($attributes)\n    {\n        $html = [];\n\n        // For numeric keys we will assume that the key and the value are the same\n        // as this will convert HTML attributes such as \"required\" to a correct\n        // form like required=\"required\" instead of using incorrect numerics.\n        foreach ((array) $attributes as $key => $value) {\n            $element = $this->attributeElement($key, $value);\n\n            if (!is_null($element)) {\n                $html[] = $element;\n            }\n        }\n\n        return count($html) > 0 ? ' '.implode(' ', $html) : '';\n    }\n\n    /**\n     * attributeElement builds a single attribute element.\n     *\n     * @param  string  $key\n     * @param  string  $value\n     * @return string\n     */\n    protected function attributeElement($key, $value)\n    {\n        if (is_numeric($key)) {\n            $key = $value;\n        }\n\n        if (is_null($value)) {\n            return;\n        }\n\n        if ($value === true) {\n            return $key;\n        }\n        elseif (is_array($value)) {\n            $value = substr(htmlspecialchars(json_encode($value), ENT_QUOTES, 'UTF-8'), 1, -1);\n        }\n        else {\n            $value = e($value);\n        }\n\n        return $key.'=\"'.$value.'\"';\n    }\n\n    /**\n     * obfuscate a string to prevent spam-bots from sniffing it.\n     * @param  string  $value\n     * @return string\n     */\n    public function obfuscate($value)\n    {\n        $safe = '';\n\n        foreach (str_split($value) as $letter) {\n            if (ord($letter) > 128) {\n                return $letter;\n            }\n\n            // To properly obfuscate the value, we will randomly convert each letter to\n            // its entity or hexadecimal representation, keeping a bot from sniffing\n            // the randomly obfuscated letters out of the string on the responses.\n            switch (rand(1, 3)) {\n                case 1:\n                    $safe .= '&#'.ord($letter).';';\n                    break;\n\n                case 2:\n                    $safe .= '&#x'.dechex(ord($letter)).';';\n                    break;\n\n                case 3:\n                    $safe .= $letter;\n            }\n        }\n\n        return $safe;\n    }\n\n    /**\n     * strip removes HTML from a string, with allowed tags, e.g. <p>\n     * @param $string\n     * @param $allow\n     * @return string\n     */\n    public static function strip($string, $allow = '')\n    {\n        return strip_tags(htmlspecialchars_decode($string), $allow);\n    }\n\n    /**\n     * limit HTML with specific length with a proper tag handling.\n     * @param string $html HTML string to limit\n     * @param int $maxLength String length to truncate at\n     * @param  string  $end\n     * @return string\n     */\n    public static function limit($html, $maxLength = 100, $end = '...')\n    {\n        $isUtf8 = true;\n        $printedLength = 0;\n        $position = 0;\n        $tags = [];\n\n        $regex = $isUtf8\n            ? '{</?([a-z]+)[^>]*>|&#?[a-zA-Z0-9]+;|[\\x80-\\xFF][\\x80-\\xBF]*}'\n            : '{</?([a-z]+)[^>]*>|&#?[a-zA-Z0-9]+;}';\n\n        $result = '';\n\n        while ($printedLength < $maxLength && preg_match($regex, $html, $match, PREG_OFFSET_CAPTURE, $position)) {\n            list($tag, $tagPosition) = $match[0];\n\n            $str = substr($html, $position, $tagPosition - $position);\n            if ($printedLength + strlen($str) > $maxLength) {\n                $result .= substr($str, 0, $maxLength - $printedLength) . $end;\n                $printedLength = $maxLength;\n                break;\n            }\n\n            $result .= $str;\n            $printedLength += strlen($str);\n            if ($printedLength >= $maxLength) {\n                $result .= $end;\n                break;\n            }\n\n            if ($tag[0] === '&' || ord($tag[0]) >= 0x80) {\n                $result .= $tag;\n                $printedLength++;\n            }\n            else {\n                $tagName = $match[1][0];\n                if ($tag[1] === '/') {\n                    $openingTag = array_pop($tags);\n                    $result .= $tag;\n                }\n                elseif ($tag[strlen($tag) - 2] === '/') {\n                    $result .= $tag;\n                }\n                else {\n                    $result .= $tag;\n                    $tags[] = $tagName;\n                }\n            }\n\n            $position = $tagPosition + strlen($tag);\n        }\n\n        if ($printedLength < $maxLength && $position < strlen($html)) {\n            $result .= substr($html, $position, $maxLength - $printedLength);\n        }\n\n        while (!empty($tags)) {\n            $result .= sprintf('</%s>', array_pop($tags));\n        }\n\n        return $result;\n    }\n\n    /**\n     * minify makes HTML more compact\n     */\n    public static function minify($html)\n    {\n        $search = [\n            // Strip whitespaces after tags, except space\n            '/\\>[^\\S ]+/s',\n            // Strip whitespaces before tags, except space\n            '/[^\\S ]+\\</s',\n            // Shorten multiple whitespace sequences\n            '/(\\s)+/s',\n            // Remove HTML comments\n            '/<!--(.|\\s)*?-->/'\n        ];\n\n        $replace = [\n            '>',\n            '<',\n            '\\\\1',\n            ''\n        ];\n\n        return preg_replace($search, $replace, $html);\n    }\n\n    /**\n     * clean HTML to prevent XSS attacks using DOM-based sanitization.\n     */\n    public static function clean(string $html): string\n    {\n        $config = (new HtmlSanitizerConfig())\n            ->allowSafeElements()\n            ->allowRelativeLinks()\n            ->allowRelativeMedias();\n\n        $sanitizer = new HtmlSanitizer($config);\n\n        return $sanitizer->sanitize($html);\n    }\n\n    /**\n     * cleanCss sanitizes CSS content to prevent injection attacks while preserving\n     * valid CSS syntax. Unlike clean() which is designed for HTML, this method handles\n     * CSS-specific threats: closing style tags, javascript: URLs, and legacy IE expressions.\n     */\n    public static function cleanCss(string $css): string\n    {\n        // Strip any HTML tags (prevents </style><script>...</script> injection)\n        $css = strip_tags($css);\n\n        // Remove CSS expressions and legacy IE behaviors\n        $css = preg_replace('/expression\\s*\\(/i', '(', $css);\n        $css = preg_replace('/behavior\\s*:/i', '', $css);\n        $css = preg_replace('/-moz-binding\\s*:/i', '', $css);\n\n        // Remove javascript: and vbscript: from url() values\n        $css = preg_replace('/url\\s*\\(\\s*[\\'\"]?\\s*(?:javascript|vbscript)\\s*:/i', 'url(invalid:', $css);\n\n        return $css;\n    }\n\n    /**\n     * cleanVector sanitizes XML/SVG content to prevent XSS attacks using DOM-based sanitization.\n     * Uses enshrined/svg-sanitize library which is ported from DOMPurify.\n     */\n    public static function cleanVector(string $html): string\n    {\n        $sanitizer = new SvgSanitizer();\n        $sanitizer->minify(false);\n        $sanitizer->removeRemoteReferences(true);\n        $sanitizer->removeXMLTag(true);\n\n        $clean = $sanitizer->sanitize($html);\n\n        return $clean !== false ? $clean : '';\n    }\n\n    /**\n     * isValidColor determines if a given string is a valid CSS color value\n     */\n    public function isValidColor(string $value): bool\n    {\n        return Str::startsWith($value, [\n            '#',\n            'var(',\n            'rgb(',\n            'rgba(',\n            'hsl('\n        ]);\n    }\n}\n"
  },
  {
    "path": "src/Html/HtmlServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Html;\n\nuse Illuminate\\Support\\ServiceProvider;\nuse Illuminate\\Contracts\\Support\\DeferrableProvider;\n\n/**\n * HtmlServiceProvider\n *\n * @package october\\html\n * @author Alexey Bobkov, Samuel Georges\n */\nclass HtmlServiceProvider extends ServiceProvider implements DeferrableProvider\n{\n    /**\n     * register the service provider.\n     */\n    public function register()\n    {\n        $this->registerHtmlBuilder();\n\n        $this->registerFormBuilder();\n\n        $this->registerBlockBuilder();\n    }\n\n    /**\n     * Register the HTML builder instance.\n     * @return void\n     */\n    protected function registerHtmlBuilder()\n    {\n        $this->app->singleton('html', function ($app) {\n            return new HtmlBuilder($app['url']);\n        });\n    }\n\n    /**\n     * Register the form builder instance.\n     * @return void\n     */\n    protected function registerFormBuilder()\n    {\n        $this->app->singleton('form', function ($app) {\n            $form = new FormBuilder($app['html'], $app['url'], $app['session.store']->token(), str_random(40));\n            return $form->setSessionStore($app['session.store']);\n        });\n    }\n\n    /**\n     * Register the Block builder instance.\n     * @return void\n     */\n    protected function registerBlockBuilder()\n    {\n        $this->app->singleton('block', function ($app) {\n            return new BlockBuilder;\n        });\n    }\n\n    /**\n     * provides gets the services provided by the provider\n     */\n    public function provides()\n    {\n        return ['html', 'form', 'block'];\n    }\n}\n"
  },
  {
    "path": "src/Html/README.md",
    "content": "## Rain Html\n\nAn extension of `illuminate\\html` and more.\n\n### HTML helpers\n\nThese additional helpers are available in the `Helper` class.\n\n**nameToArray**\n\nConverts a HTML array string to a PHP array. Empty values are removed.\n\n```php\n// Converts to PHP array ['user', 'location', 'city']\n$array = Helper::nameToArray('user[location][city]');\n```\n\n**strip**\n\nRemoves HTML from a string.\n```php\n// Outputs: Fatal Error! Oh noes!\necho Html::strip('<b>Fatal Error!</b> Oh noes!');\n```\n"
  },
  {
    "path": "src/Html/UrlMixin.php",
    "content": "<?php namespace October\\Rain\\Html;\n\nuse File;\nuse Config;\n\n/**\n * UrlMixin\n *\n * @package october\\html\n * @author Alexey Bobkov, Samuel Georges\n */\nclass UrlMixin\n{\n    use \\Illuminate\\Support\\InteractsWithTime;\n\n    /**\n     * @var mixed provider\n     */\n    protected $provider;\n\n    /**\n     * __construct\n     */\n    public function __construct($provider)\n    {\n        $this->provider = $provider;\n    }\n\n    /**\n     * makeRelative converts a full URL to a relative URL\n     */\n    public function makeRelative($url)\n    {\n        $fullUrl = $this->provider->to($url);\n        return parse_url($fullUrl, PHP_URL_PATH)\n            . (($query = parse_url($fullUrl, PHP_URL_QUERY)) ? '?' . $query : '')\n            . (($fragment = parse_url($fullUrl, PHP_URL_FRAGMENT)) ? '#' . $fragment : '')\n            ?: '/';\n    }\n\n    /**\n     * toRelative makes a link relative if configuration asks for it\n     */\n    public function toRelative($url)\n    {\n        return Config::get('system.relative_links', false)\n            ? $this->makeRelative($url)\n            : $this->provider->to($url);\n    }\n\n    /**\n     * toSigned signs a bare URL that can be validated with hasValidSignature\n     */\n    public function toSigned($url, $expiration = null, $absolute = true)\n    {\n        if (!$absolute) {\n            $url = $this->makeRelative($url);\n        }\n\n        $parameters = [];\n\n        $parts = parse_url($url);\n\n        parse_str($parts['query'] ?? '', $parameters);\n\n        unset($parameters['signature']);\n\n        ksort($parameters);\n\n        if ($expiration) {\n            unset($parameters['expires']);\n            $parameters = $parameters + ['expires' => $this->availableAt($expiration)];\n        }\n\n        $key = Config::get('app.key');\n\n        $signUrl = http_build_url($url, ['query' => http_build_query($parameters)]);\n\n        $signature = hash_hmac('sha256', $signUrl, $key);\n\n        return http_build_url($url, ['query' => http_build_query($parameters + ['signature' => $signature])]);\n    }\n\n    /**\n     * assetVersion takes a disk path, resolves it to a public URL, and appends\n     * a cache-busting version query string based on the file's modification time.\n     *\n     * Supports path symbols: ~ (base), $ (plugins), # (themes)\n     *\n     *     Url::assetVersion('~/themes/demo/assets/js/app.js')\n     *     // → http://localhost/themes/demo/assets/js/app.js?v1a2b3c4d\n     */\n    public function assetVersion(string $path): string\n    {\n        // External URLs pass through unchanged\n        if (str_starts_with($path, '//') || str_starts_with($path, 'http://') || str_starts_with($path, 'https://')) {\n            return $path;\n        }\n\n        // Already has a query string, skip versioning\n        if (str_contains($path, '?')) {\n            return $path;\n        }\n\n        // Resolve path symbols (~, $, #) to filesystem path\n        $filePath = File::symbolizePath($path);\n\n        // Ensure absolute filesystem path\n        if (!str_starts_with($filePath, base_path())) {\n            $filePath = base_path(ltrim($filePath, '/'));\n        }\n\n        // Compute version hash from file modification time\n        if (is_file($filePath)) {\n            $version = hash('crc32', (string) filemtime($filePath));\n        }\n        else {\n            $version = hash('crc32', (string) filemtime(base_path('vendor/autoload.php')));\n        }\n\n        // Convert disk path to public-facing relative path\n        $publicPath = $filePath;\n        $basePath = base_path();\n        if (str_starts_with($publicPath, $basePath)) {\n            $publicPath = substr($publicPath, strlen($basePath));\n        }\n\n        // Normalize directory separators for URL\n        $publicPath = str_replace('\\\\', '/', $publicPath);\n\n        // Generate public URL (respects app.asset_url for CDN/S3)\n        $url = $this->provider->asset($publicPath);\n\n        return $url . '?v' . $version;\n    }\n}\n"
  },
  {
    "path": "src/Html/UrlServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Html;\n\nuse Illuminate\\Support\\ServiceProvider;\n\n/**\n * UrlServiceProvider\n *\n * @package october\\html\n * @author Alexey Bobkov, Samuel Georges\n */\nclass UrlServiceProvider extends ServiceProvider\n{\n    /**\n     * register the service provider.\n     */\n    public function register()\n    {\n        $this->registerRelativeHelpers();\n        $this->registerRequestHelpers();\n\n        $this->registerUrlGeneratorPolicy();\n\n        $this->app['events']->listen('site.changed', function() {\n            $this->registerUrlGeneratorPolicy();\n        });\n    }\n\n    /**\n     * registerUrlGeneratorPolicy controls how URL links are generated throughout the application.\n     *\n     * detect   - detect hostname and use the current schema\n     * secure   - detect hostname and force HTTPS schema\n     * insecure - detect hostname and force HTTP schema\n     * force    - force hostname and schema using app.url config value\n     */\n    protected function registerUrlGeneratorPolicy()\n    {\n        $provider = $this->app['url'];\n        $policy = $this->app['config']->get('system.link_policy', 'detect');\n        $appUrl = $this->app['config']->get('app.url');\n\n        switch (strtolower($policy)) {\n            case 'force':\n                $provider->forceRootUrl($appUrl);\n                $provider->forceScheme(str_starts_with($appUrl, 'http://') ? 'http' : 'https');\n                break;\n\n            case 'insecure':\n                $provider->forceScheme('http');\n                break;\n\n            case 'secure':\n                $provider->forceScheme('https');\n                break;\n        }\n\n        // Workaround for October CMS installed to a subdirectory since\n        // Laravel won't support this use case, related issue:\n        // https://github.com/laravel/framework/pull/3918\n        if ($this->app->runningInConsole()) {\n            $provider->forceRootUrl($appUrl);\n        }\n    }\n\n    /**\n     * registerRelativeHelpers\n     */\n    protected function registerRelativeHelpers()\n    {\n        $provider = $this->app['url'];\n\n        $provider->macro('makeRelative', function(...$args) use ($provider) {\n            return (new \\October\\Rain\\Html\\UrlMixin($provider))->makeRelative(...$args);\n        });\n\n        $provider->macro('toRelative', function(...$args) use ($provider) {\n            return (new \\October\\Rain\\Html\\UrlMixin($provider))->toRelative(...$args);\n        });\n\n        $provider->macro('toSigned', function(...$args) use ($provider) {\n            return (new \\October\\Rain\\Html\\UrlMixin($provider))->toSigned(...$args);\n        });\n\n        $provider->macro('assetVersion', function(...$args) use ($provider) {\n            return (new \\October\\Rain\\Html\\UrlMixin($provider))->assetVersion(...$args);\n        });\n    }\n\n    /**\n     * registerRequestHelpers\n     */\n    protected function registerRequestHelpers()\n    {\n        $provider = $this->app['request'];\n\n        $provider->macro('pjaxCached', function() use ($provider) {\n            return $provider->headers->get('X-PJAX-CACHED') == true;\n        });\n\n        $provider->macro('isCrawler', function($userAgent = null) use ($provider) {\n            return (new \\Jaybizzle\\CrawlerDetect\\CrawlerDetect($provider->server()))\n                ->isCrawler($userAgent)\n            ;\n        });\n    }\n}\n"
  },
  {
    "path": "src/Installer/Console/OctoberBuild.php",
    "content": "<?php\n\nnamespace October\\Rain\\Installer\\Console;\n\nuse Lang;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Illuminate\\Console\\Command;\n\n/**\n * OctoberBuild installs the necessary core packages\n *\n * @package october\\system\n * @author Alexey Bobkov, Samuel Georges\n */\nclass OctoberBuild extends Command\n{\n    use \\October\\Rain\\Installer\\Traits\\SetupHelper;\n    use \\October\\Rain\\Installer\\Traits\\SetupBuilder;\n\n    /**\n     * The console command name.\n     */\n    protected $name = 'october:build';\n\n    /**\n     * The console command description.\n     */\n    protected $description = 'Installs the necessary core packages';\n\n    /**\n     * Execute the console command.\n     */\n    public function handle()\n    {\n        // Installing Dependencies\n        $this->output->section(Lang::get('system::lang.installer.dependencies_section'));\n\n        $this->setupInstallOctober();\n\n        $this->outputOutro();\n    }\n\n    /**\n     * getOptions get the console command options\n     */\n    protected function getOptions()\n    {\n        return [\n            ['want', 'w', InputOption::VALUE_REQUIRED, 'Provide a custom version.'],\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Installer/Console/OctoberInstall.php",
    "content": "<?php\n\nnamespace October\\Rain\\Installer\\Console;\n\nuse PDO;\nuse Lang;\nuse Config;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Illuminate\\Console\\Command;\nuse Exception;\n\n/**\n * OctoberInstall is a console command to install October CMS\n *\n * This sets up October for the first time. It will prompt the user for several\n * configuration items, including application URL and database config, and then\n * perform a database migration.\n *\n * @package october\\system\n * @author Alexey Bobkov, Samuel Georges\n */\nclass OctoberInstall extends Command\n{\n    use \\October\\Rain\\Installer\\Traits\\SetupHelper;\n    use \\October\\Rain\\Installer\\Traits\\SetupBuilder;\n\n    /**\n     * @var string name is the console command name\n     */\n    protected $name = 'october:install';\n\n    /**\n     * @var string description is the console command description\n     */\n    protected $description = 'Set up October CMS for the first time.';\n\n    /**\n     * @var int keyRetries counts license key attempts before giving up\n     */\n    protected $keyRetries = 0;\n\n    /**\n     * handle executes the console command\n     */\n    public function handle()\n    {\n        if (!$this->input->isInteractive()) {\n            return $this->handleNonInteractive();\n        }\n\n        if (!$this->checkEnvWritable()) {\n            $this->output->error('Cannot write to .env file. Check file permissions and try again.');\n            return 1;\n        }\n\n        $this->outputIntro();\n        $this->setupEncryptionKey();\n        $this->outputLanguageTable();\n        $this->setupLanguage();\n\n        if ($this->nonInteractiveCheck()) {\n            $this->outputNonInteractive();\n            return 1;\n        }\n\n        // Application Configuration\n        $this->output->section(Lang::get('system::lang.installer.app_config_section'));\n        $this->setupApplicationUrls();\n        $this->setupDatabaseConfig();\n\n        // Demo Theme\n        $this->output->section(Lang::get('system::lang.installer.demo_section'));\n        $this->setupDemoTheme();\n\n        // License Key\n        $this->output->section(Lang::get('system::lang.installer.license_section'));\n        $this->setupLicenseKey();\n\n        // Installing Dependencies\n        $this->output->section(Lang::get('system::lang.installer.dependencies_section'));\n        $this->setupInstallOctober();\n\n        // $this->output->section('Migrating Database');\n        // $this->refreshEnvVars();\n        // $this->setupMigrateDatabase();\n\n        $this->outputOutro();\n    }\n\n    /**\n     * handleNonInteractive processes the install without any prompts\n     */\n    protected function handleNonInteractive()\n    {\n        $this->line('Installing October CMS (non-interactive)...');\n        $this->line('');\n\n        $errCode = null;\n\n        $this->comment('Migrating database...');\n        passthru('php artisan october:migrate', $errCode);\n        if ($errCode !== 0) {\n            $this->output->error('october:migrate failed.');\n            return 1;\n        }\n        $this->line('');\n\n        $this->comment('Migrating tailor...');\n        passthru('php artisan tailor:migrate', $errCode);\n        if ($errCode !== 0) {\n            $this->output->error('tailor:migrate failed.');\n            return 1;\n        }\n        $this->line('');\n\n        $this->comment('Seeding demo theme...');\n        passthru('php artisan theme:seed demo', $errCode);\n        if ($errCode !== 0) {\n            $this->output->error('theme:seed failed.');\n            return 1;\n        }\n        $this->line('');\n\n        $this->comment('Setting build number...');\n        passthru('php artisan october:util set build', $errCode);\n        if ($errCode !== 0) {\n            $this->output->error('october:util set build failed.');\n            return 1;\n        }\n        $this->line('');\n\n        $this->output->success('October CMS installed successfully.');\n    }\n\n    /**\n     * setupDemoTheme\n     */\n    protected function setupDemoTheme()\n    {\n        // Install the demo theme and content?\n        $this->setDemoContent(\n            $this->confirm(Lang::get('system::lang.installer.install_demo_label'), true)\n        );\n    }\n\n    /**\n     * setupLanguage asks the user their language preference\n     */\n    protected function setupLanguage()\n    {\n        try {\n            $locale = strtolower($this->ask(\n                // Select Language\n                Lang::get('system::lang.installer.locale_select_label'),\n                env('APP_LOCALE', 'en')\n            ));\n\n            $availableLocales = $this->getAvailableLocales();\n            if (!isset($availableLocales[$locale])) {\n                throw new Exception(\"Language code '{$locale}' is invalid, please try again\");\n            }\n\n            $this->setEnvVar('APP_LOCALE', $locale);\n            Lang::setLocale($locale);\n        }\n        catch (Exception $ex) {\n            $this->output->error($ex->getMessage());\n            return $this->setupLanguage();\n        }\n    }\n\n    /**\n     * outputLanguageTable displays a 2 column table with the available locales\n     */\n    protected function outputLanguageTable()\n    {\n        $locales = $this->getAvailableLocales();\n\n        $tableRows = [];\n        $halfCount = count($locales) / 2;\n        $i = 0;\n        foreach ($locales as $locale => $info) {\n            if ($i < $halfCount) {\n                $tableRows[$i] = [$locale, \"({$info[1]}) {$info[0]}\"];\n            }\n            else {\n                $tableRows[$i-$halfCount] = array_merge(\n                    $tableRows[$i-$halfCount],\n                    [$locale, \"({$info[1]}) {$info[0]}\"]\n                );\n            }\n            $i++;\n        }\n\n        $this->output->table(['Code', 'Language', 'Code', 'Language'], $tableRows);\n    }\n\n    /**\n     * setupApplicationUrls asks for URL based configuration\n     */\n    protected function setupApplicationUrls()\n    {\n        $url = $this->ask(\n            // Application URL\n            Lang::get('system::lang.installer.app_url_label'),\n            env('APP_URL', 'http://localhost')\n        );\n        $url = rtrim(trim($url), '/');\n        $this->setEnvVar('APP_URL', $url);\n\n        // To secure your application, use a custom address for accessing the admin panel.\n        $this->comment(Lang::get('system::lang.installer.backend_uri_comment'));\n        $backendUri = $this->ask(\n            // Backend URI\n            Lang::get('system::lang.installer.backend_uri_label'),\n            env('BACKEND_URI', '/admin')\n        );\n\n        if ($backendUri[0] !== '/') {\n            $backendUri = '/'.$backendUri;\n        }\n\n        $this->setEnvVar('BACKEND_URI', $backendUri);\n    }\n\n    /**\n     * setupEncryptionKey sets the application encryption key if not set already\n     */\n    protected function setupEncryptionKey()\n    {\n        if (env('APP_KEY')) {\n            return;\n        }\n\n        $key = $this->getRandomKey();\n        $this->setEnvVar('APP_KEY', $key);\n        Config::set('app.key', $key);\n\n        $this->info(sprintf('Application key [%s] set successfully.', $key));\n    }\n\n    /**\n     * setupDatabaseConfig requests the database engine\n     */\n    protected function setupDatabaseConfig()\n    {\n        $typeMap = [\n            'SQLite' => 'sqlite',\n            'MySQL' => 'mysql',\n            'Postgres' => 'pgsql',\n            'SQL Server' => 'sqlsrv',\n        ];\n\n        $currentDriver = array_flip($typeMap)[$this->getEnvVar('DB_CONNECTION')] ?? 'SQLite';\n\n        $type = $this->choice(\n            // Database Engine\n            Lang::get('system::lang.installer.database_engine_label'),\n            [\n                'SQLite',\n                'MySQL',\n                'Postgres',\n                'SQL Server'\n            ],\n            $currentDriver\n        );\n\n        $driver = $typeMap[$type] ?? 'sqlite';\n\n        $this->setEnvVar('DB_CONNECTION', $driver);\n        Config::set('database.default', $driver);\n\n        if ($driver === 'sqlite') {\n            $this->setupDatabaseSqlite();\n        }\n        else {\n            $this->setupDatabase($driver);\n        }\n\n        // Validate database connection\n        try {\n            $this->checkDatabaseFromConfig($driver);\n        }\n        catch (Exception $ex) {\n            $this->output->error($ex->getMessage());\n            return $this->setupDatabaseConfig();\n        }\n    }\n\n    /**\n     * checkDatabaseFromConfig validates the supplied database config\n     * and throws an exception if something is wrong\n     */\n    protected function checkDatabaseFromConfig($driver)\n    {\n        $this->checkDatabase(\n            $driver,\n            Config::get(\"database.connections.{$driver}.host\"),\n            Config::get(\"database.connections.{$driver}.port\"),\n            Config::get(\"database.connections.{$driver}.database\"),\n            Config::get(\"database.connections.{$driver}.username\"),\n            Config::get(\"database.connections.{$driver}.password\")\n        );\n    }\n\n    /**\n     * setupDatabase sets up a SQL based database\n     */\n    protected function setupDatabase($driver)\n    {\n        // Hostname for the database connection.\n        $this->comment(Lang::get('system::lang.installer.database_host_comment'));\n        $host = $this->ask(\n            // Database Host\n            Lang::get('system::lang.installer.database_host_label'),\n            $this->getEnvVar('DB_HOST', 'localhost')\n        );\n        $this->setEnvVar('DB_HOST', $host);\n        Config::set(\"database.connections.{$driver}.host\", $host);\n\n        // (Optional) A port for the connection.\n        $this->comment(Lang::get('system::lang.installer.database_port_comment'));\n        $port = $this->ask(\n            // Database Port\n            Lang::get('system::lang.installer.database_port_label'),\n            $this->getEnvVar('DB_PORT', false)\n        ) ?: '';\n        $this->setEnvVar('DB_PORT', $port);\n        Config::set(\"database.connections.{$driver}.port\", $port);\n\n        // Specify the name of the database to use.\n        $this->comment(Lang::get('system::lang.installer.database_name_comment'));\n        $database = $this->ask(\n            // Database Name\n            Lang::get('system::lang.installer.database_name_label'),\n            $this->getEnvVar('DB_DATABASE', 'octobercms')\n        );\n        $this->setEnvVar('DB_DATABASE', $database);\n        Config::set(\"database.connections.{$driver}.database\", $database);\n\n        // User with create database privileges.\n        $this->comment(Lang::get('system::lang.installer.database_login_comment'));\n        $username = $this->ask(\n            // Database Login\n            Lang::get('system::lang.installer.database_login_label'),\n            $this->getEnvVar('DB_USERNAME', 'root')\n        );\n        $this->setEnvVar('DB_USERNAME', $username);\n        Config::set(\"database.connections.{$driver}.username\", $username);\n\n        // Password for the specified user.\n        $this->comment(Lang::get('system::lang.installer.database_pass_comment'));\n        $password = $this->ask(\n            // Database Password\n            Lang::get('system::lang.installer.database_pass_label'),\n            $this->getEnvVar('DB_PASSWORD', false)\n        ) ?: '';\n        $this->setEnvVar('DB_PASSWORD', $password);\n        Config::set(\"database.connections.{$driver}.password\", $password);\n    }\n\n    /**\n     * setupDatabaseSqlite sets up the SQLite database engine\n     */\n    protected function setupDatabaseSqlite()\n    {\n        // For file-based storage, enter a path relative to the application root directory.\n        $this->comment(Lang::get('system::lang.installer.database_path_comment'));\n\n        $defaultDb = $this->getEnvVar('DB_DATABASE', 'storage/database.sqlite');\n        if ($defaultDb === 'database') {\n            $defaultDb = 'storage/database.sqlite';\n        }\n\n        $filename = $this->ask(\n            // Database Path\n            Lang::get('system::lang.installer.database_path_label'),\n            $defaultDb\n        );\n        $this->setEnvVar('DB_DATABASE', $filename);\n        Config::set(\"database.connections.sqlite.database\", $filename);\n\n        try {\n            if (!file_exists($filename)) {\n                $directory = dirname($filename);\n                if (!is_dir($directory)) {\n                    mkdir($directory, 0777, true);\n                }\n\n                new PDO('sqlite:'.$filename);\n            }\n        }\n        catch (Exception $ex) {\n            $this->output->error($ex->getMessage());\n            return $this->setupDatabaseSqlite();\n        }\n\n        return ['database' => $filename];\n    }\n\n    /**\n     * setupLicenseKey asks for the license key\n     */\n    protected function setupLicenseKey()\n    {\n        if ($this->keyRetries++ > 10) {\n            // Too many failed attempts\n            $this->output->error(Lang::get('system::lang.installer.too_many_failures_label'));\n\n            $this->outputNonInteractive();\n            exit(1);\n        }\n\n        // Enter a valid License Key to proceed.\n        $this->comment(Lang::get('system::lang.installer.license_key_comment'));\n\n        // License Key\n        $licenseKey = trim($this->ask(Lang::get('system::lang.installer.license_key_label')));\n        if (!strlen($licenseKey)) {\n            return $this->setupLicenseKey();\n        }\n\n        try {\n            $this->setupSetProject($licenseKey);\n\n            // Thanks for being a customer of October CMS!\n            $this->output->success(Lang::get('system::lang.installer.license_thanks_comment'));\n        }\n        catch (Exception $ex) {\n            $this->output->error($ex->getMessage());\n            return $this->setupLicenseKey();\n        }\n    }\n\n    /**\n     * setupMigrateDatabase migrates the database\n     */\n    protected function setupMigrateDatabase()\n    {\n        $errCode = null;\n        $exec = 'php artisan october:migrate';\n        $this->comment(\"Executing: {$exec}\");\n        $this->line('');\n\n        passthru($exec, $errCode);\n\n        if ($errCode !== 0) {\n            $this->outputFailedOutro();\n            exit(1);\n        }\n    }\n\n    protected function outputNonInteractive()\n    {\n        // Too many failed attempts\n        $this->output->error(Lang::get('system::lang.installer.non_interactive_label'));\n\n        // If you see this error immediately, use these non-interactive commands instead.\n        $this->comment(Lang::get('system::lang.installer.non_interactive_comment'));\n        $this->line('');\n\n        // Open this application in your browser\n        $this->line(Lang::get('system::lang.installer.open_configurator_comment'));\n        $this->line('');\n\n        $this->line('-- OR --');\n        $this->line('');\n\n        $this->line(\"* php artisan project:set <LICENSE KEY>\");\n        $this->line('');\n\n        if ($want = $this->option('want')) {\n            $this->line(\"* php artisan october:build --want=\".$want);\n        }\n        else {\n            $this->line(\"* php artisan october:build\");\n        }\n    }\n\n    /**\n     * getOptions get the console command options\n     */\n    protected function getOptions()\n    {\n        return [\n            ['want', 'w', InputOption::VALUE_REQUIRED, 'Provide a custom version.'],\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Installer/GatewayClient.php",
    "content": "<?php\n\nnamespace October\\Rain\\Installer;\n\nuse Exception;\n\n/**\n * Gateway Client\n *\n * A standalone PHP client for the October CMS Gateway V1 API.\n * No external dependencies required - works with PHP 7.4+\n *\n * Usage:\n *   // Account-level operations (HMAC auth)\n *   $client = (new GatewayClient)->setCredentials('your-api-key', 'your-api-secret');\n *   $projects = $client->listProjects();\n *\n *   // Project-level operations (uses license key or auth.json hash)\n *   $client = (new GatewayClient)->setProjectHash('your-license-key-or-hash');\n *   $updates = $client->checkForUpdates(['plugins' => [...], 'themes' => [...]]);\n *\n *\n */\nclass GatewayClient\n{\n    /**\n     * @var string API base URL\n     */\n    const API_BASE_URL = 'https://api.octobercms.com';\n\n    /**\n     * @var string API version prefix\n     */\n    const API_VERSION = 'v1';\n\n    /**\n     * @var string|null apiKey for HMAC authentication\n     */\n    protected $apiKey;\n\n    /**\n     * @var string|null apiSecret for HMAC authentication\n     */\n    protected $apiSecret;\n\n    /**\n     * @var string|null projectHash for project-level authentication (license key or auth.json hash)\n     */\n    protected $projectHash;\n\n    /**\n     * @var int timeout in seconds\n     */\n    protected $timeout = 30;\n\n    /**\n     * @var array lastResponseHeaders\n     */\n    protected $lastResponseHeaders = [];\n\n    /**\n     * @var int lastStatusCode\n     */\n    protected $lastStatusCode = 0;\n\n    /**\n     * @var string|null lastErrorCode from API response\n     */\n    protected $lastErrorCode;\n\n    // =========================================================================\n    // CONFIGURATION\n    // =========================================================================\n\n    /**\n     * setCredentials for account-level authentication\n     */\n    public function setCredentials(string $apiKey, string $apiSecret): self\n    {\n        $this->apiKey = $apiKey;\n        $this->apiSecret = $apiSecret;\n        return $this;\n    }\n\n    /**\n     * setProjectHash for project-level authentication\n     *\n     * @param string $projectHash License key or auth.json hash (bind code)\n     */\n    public function setProjectHash(string $projectHash): self\n    {\n        $this->projectHash = $projectHash;\n        return $this;\n    }\n\n    /**\n     * setTimeout for requests\n     */\n    public function setTimeout(int $seconds): self\n    {\n        $this->timeout = $seconds;\n        return $this;\n    }\n\n    // =========================================================================\n    // ACCOUNT-LEVEL ENDPOINTS (HMAC Auth Required)\n    // =========================================================================\n\n    /**\n     * createProject creates a new project\n     *\n     * @param string $name Project name\n     * @param array $options Optional: description\n     * @return array Project data including project_id and license_key\n     * @throws Exception\n     */\n    public function createProject(string $name, array $options = []): array\n    {\n        return $this->requestWithHmac('projects/create', array_merge(\n            ['name' => $name],\n            $options\n        ));\n    }\n\n    /**\n     * updateProject updates an existing project\n     *\n     * @param int $projectId Project ID\n     * @param array $options Optional: name, description\n     * @return array Updated project data\n     * @throws Exception\n     */\n    public function updateProject(int $projectId, array $options = []): array\n    {\n        return $this->requestWithHmac('projects/update', array_merge(\n            ['project_id' => $projectId],\n            $options\n        ));\n    }\n\n    /**\n     * deleteProject deletes a project\n     *\n     * @param int $projectId Project ID\n     * @return array Confirmation\n     * @throws Exception\n     */\n    public function deleteProject(int $projectId): array\n    {\n        return $this->requestWithHmac('projects/delete', [\n            'project_id' => $projectId,\n        ]);\n    }\n\n    /**\n     * listProjects returns all projects for the account\n     *\n     * @param int $limit Results per page (1-100)\n     * @param string|null $cursor Pagination cursor\n     * @return array Projects list with next_cursor\n     * @throws Exception\n     */\n    public function listProjects(int $limit = 50, ?string $cursor = null): array\n    {\n        $params = ['limit' => $limit];\n        if ($cursor !== null) {\n            $params['cursor'] = $cursor;\n        }\n        return $this->requestWithHmac('projects/list', $params);\n    }\n\n    /**\n     * getProject returns detailed information about a project\n     *\n     * @param int $projectId Project ID\n     * @return array Project details\n     * @throws Exception\n     */\n    public function getProject(int $projectId): array\n    {\n        return $this->requestWithHmac('projects/get', [\n            'project_id' => $projectId,\n        ]);\n    }\n\n    /**\n     * lookupByDomain looks up a project by domain name\n     *\n     * @param string $domainName Domain to look up\n     * @return array Project ID\n     * @throws Exception\n     */\n    public function lookupByDomain(string $domainName): array\n    {\n        return $this->requestWithHmac('licenses/domain', [\n            'domain_name' => $domainName,\n        ]);\n    }\n\n    /**\n     * rotateLicense regenerates a license key\n     *\n     * @param int $projectId Project ID\n     * @return array New license_key and license_hash\n     * @throws Exception\n     */\n    public function rotateLicense(int $projectId): array\n    {\n        return $this->requestWithHmac('licenses/rotate', [\n            'project_id' => $projectId,\n        ]);\n    }\n\n    /**\n     * attachPackage attaches a package to a project\n     *\n     * @param int $projectId Project ID\n     * @param string $packageCode Package code (e.g., 'Author.PluginName')\n     * @param string $type Package type ('plugin' or 'theme')\n     * @return array Confirmation with attached, package, type\n     * @throws Exception\n     */\n    public function attachPackage(int $projectId, string $packageCode, string $type): array\n    {\n        return $this->requestWithHmac('projects/packages/attach', [\n            'project_id' => $projectId,\n            'package' => $packageCode,\n            'type' => $type,\n        ]);\n    }\n\n    /**\n     * detachPackage detaches a package from a project\n     *\n     * @param int $projectId Project ID\n     * @param string $packageCode Package code (e.g., 'Author.PluginName')\n     * @param string $type Package type ('plugin' or 'theme')\n     * @return array Confirmation with detached, package, type\n     * @throws Exception\n     */\n    public function detachPackage(int $projectId, string $packageCode, string $type): array\n    {\n        return $this->requestWithHmac('projects/packages/detach', [\n            'project_id' => $projectId,\n            'package' => $packageCode,\n            'type' => $type,\n        ]);\n    }\n\n    // =========================================================================\n    // PROJECT-LEVEL ENDPOINTS\n    // =========================================================================\n\n    /**\n     * getProjectDetail returns project details (project-level auth)\n     *\n     * @param string|null $projectHash Optional project hash override\n     * @return array Project details\n     * @throws Exception\n     */\n    public function getProjectDetail(?string $projectHash = null): array\n    {\n        return $this->requestWithProject('project/detail', [\n            'id' => $projectHash ?? $this->projectHash,\n        ]);\n    }\n\n    /**\n     * checkForUpdates checks for available updates (project-level auth)\n     *\n     * @param array $options plugins (assoc array code=>version), themes, version, build\n     * @return array Available updates\n     * @throws Exception\n     */\n    public function checkForUpdates(array $options = []): array\n    {\n        $params = [];\n\n        if (isset($options['plugins'])) {\n            $params['plugins'] = base64_encode(json_encode($options['plugins']));\n        }\n        if (isset($options['themes'])) {\n            $params['themes'] = base64_encode(json_encode($options['themes']));\n        }\n        if (isset($options['version'])) {\n            $params['version'] = $options['version'];\n        }\n        if (isset($options['build'])) {\n            $params['build'] = $options['build'];\n        }\n\n        return $this->requestWithProject('project/check', $params);\n    }\n\n    /**\n     * getPackages returns multiple package details\n     *\n     * @param array $codes Package codes\n     * @param string $type Package type (plugin or theme)\n     * @return array Package information\n     * @throws Exception\n     */\n    public function getPackages(array $codes, string $type = 'plugin'): array\n    {\n        return $this->request('package/details', [\n            'names' => $codes,\n            'type' => $type,\n        ]);\n    }\n\n    /**\n     * getPackage returns single package details\n     *\n     * @param string $code Package code\n     * @param string $type Package type\n     * @return array Package information with requirements\n     * @throws Exception\n     */\n    public function getPackage(string $code, string $type = 'plugin'): array\n    {\n        return $this->requestWithProject('package/detail', [\n            'name' => $code,\n            'type' => $type,\n        ]);\n    }\n\n    /**\n     * getPackageContent returns package documentation content\n     *\n     * @param string $code Package code\n     * @param string $type Package type\n     * @return array Package info with HTML content\n     * @throws Exception\n     */\n    public function getPackageContent(string $code, string $type = 'plugin'): array\n    {\n        return $this->requestWithProject('package/content', [\n            'name' => $code,\n            'type' => $type,\n        ]);\n    }\n\n    /**\n     * browsePackages returns a paginated list of packages\n     *\n     * @param int $page Page number\n     * @param string $type Package type\n     * @param int|null $version Compatibility version filter\n     * @return array Paginated package list\n     * @throws Exception\n     */\n    public function browsePackages(int $page = 1, string $type = 'plugin', ?int $version = null): array\n    {\n        $params = [\n            'page' => $page,\n            'type' => $type,\n        ];\n        if ($version !== null) {\n            $params['version'] = $version;\n        }\n        return $this->request('package/browse', $params);\n    }\n\n    /**\n     * searchPackages searches for packages\n     *\n     * @param string $query Search query\n     * @param string|null $type Package type filter\n     * @return array Matching packages\n     * @throws Exception\n     */\n    public function searchPackages(string $query, ?string $type = null): array\n    {\n        $params = ['query' => $query];\n        if ($type !== null) {\n            $params['type'] = $type;\n        }\n        return $this->request('package/search', $params);\n    }\n\n    /**\n     * getInstallDetail returns installation details\n     *\n     * @return array Core and package hashes\n     * @throws Exception\n     */\n    public function getInstallDetail(): array\n    {\n        return $this->requestWithProject('install/detail', []);\n    }\n\n    // =========================================================================\n    // HTTP CLIENT\n    // =========================================================================\n\n    /**\n     * requestWithHmac makes a request with HMAC authentication\n     */\n    protected function requestWithHmac(string $endpoint, array $params): array\n    {\n        if (!$this->apiKey || !$this->apiSecret) {\n            throw new Exception('API credentials required for this operation');\n        }\n\n        // Add nonce\n        $params['nonce'] = (string) round(microtime(true) * 1000);\n\n        // Build query string for signing\n        $queryString = http_build_query($params, '', '&');\n\n        // Compute signature\n        $signature = base64_encode(\n            hash_hmac('sha512', $queryString, base64_decode($this->apiSecret), true)\n        );\n\n        $headers = [\n            'Rest-Key: ' . $this->apiKey,\n            'Rest-Sign: ' . $signature,\n        ];\n\n        return $this->request($endpoint, $params, 'POST', $headers);\n    }\n\n    /**\n     * requestWithProject makes a request with project-level authentication\n     */\n    protected function requestWithProject(string $endpoint, array $params): array\n    {\n        $headers = [];\n\n        if ($this->projectHash) {\n            $headers[] = 'php-auth-pw: ' . $this->projectHash;\n            $params['project'] = $this->projectHash;\n        }\n\n        return $this->request($endpoint, $params, 'POST', $headers);\n    }\n\n    /**\n     * request makes an HTTP request\n     *\n     * @param string $endpoint API endpoint\n     * @param array $params Request parameters\n     * @param string $method HTTP method\n     * @param array $headers Additional headers\n     * @return array Decoded response\n     * @throws Exception\n     */\n    protected function request(string $endpoint, array $params = [], string $method = 'POST', array $headers = []): array\n    {\n        $url = self::API_BASE_URL . '/' . self::API_VERSION . '/' . ltrim($endpoint, '/');\n\n        // Default headers\n        $headers = array_merge([\n            'Content-Type: application/x-www-form-urlencoded',\n            'Accept: application/json',\n        ], $headers);\n\n        // Initialize cURL\n        $ch = curl_init();\n\n        if ($method === 'GET') {\n            if (!empty($params)) {\n                $url .= '?' . http_build_query($params);\n            }\n        }\n        else {\n            curl_setopt($ch, CURLOPT_POST, true);\n            curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params, '', '&'));\n        }\n\n        curl_setopt_array($ch, [\n            CURLOPT_URL => $url,\n            CURLOPT_RETURNTRANSFER => true,\n            CURLOPT_TIMEOUT => $this->timeout,\n            CURLOPT_HTTPHEADER => $headers,\n            CURLOPT_HEADER => true,\n            CURLOPT_SSL_VERIFYPEER => true,\n        ]);\n\n        $response = curl_exec($ch);\n\n        if ($response === false) {\n            $error = curl_error($ch);\n            throw new Exception('cURL error: ' . $error);\n        }\n\n        $this->lastStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);\n        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);\n\n        // Parse headers and body\n        $headerString = substr($response, 0, $headerSize);\n        $body = substr($response, $headerSize);\n\n        $this->lastResponseHeaders = $this->parseHeaders($headerString);\n        $this->lastErrorCode = null;\n\n        // Decode JSON response\n        $data = json_decode($body, true);\n\n        if (json_last_error() !== JSON_ERROR_NONE) {\n            throw new Exception('Invalid JSON response: ' . $body);\n        }\n\n        // Check for error responses\n        if ($this->lastStatusCode >= 400) {\n            $this->lastErrorCode = $data['error'] ?? 'unknown_error';\n            $message = $data['message'] ?? 'An error occurred';\n            throw new Exception($message, $this->lastStatusCode);\n        }\n\n        return $data;\n    }\n\n    /**\n     * parseHeaders extracts headers from response\n     */\n    protected function parseHeaders(string $headerString): array\n    {\n        $headers = [];\n        foreach (explode(\"\\r\\n\", $headerString) as $line) {\n            if (strpos($line, ':') !== false) {\n                list($key, $value) = explode(':', $line, 2);\n                $headers[trim($key)] = trim($value);\n            }\n        }\n        return $headers;\n    }\n\n    /**\n     * getLastResponseSignature returns the last response signature for verification\n     */\n    public function getLastResponseSignature(): ?string\n    {\n        return $this->lastResponseHeaders['Rest-Sign'] ?? null;\n    }\n\n    /**\n     * getLastStatusCode returns the last HTTP status code\n     */\n    public function getLastStatusCode(): int\n    {\n        return $this->lastStatusCode;\n    }\n\n    /**\n     * getRetryAfter returns the Retry-After header value (for rate limiting)\n     */\n    public function getRetryAfter(): ?int\n    {\n        $value = $this->lastResponseHeaders['Retry-After'] ?? null;\n        return $value !== null ? (int) $value : null;\n    }\n\n    /**\n     * getLastErrorCode returns the API error code from the last failed request\n     */\n    public function getLastErrorCode(): ?string\n    {\n        return $this->lastErrorCode;\n    }\n\n    /**\n     * isRateLimited checks if the last error was a rate limit error\n     */\n    public function isRateLimited(): bool\n    {\n        return $this->lastErrorCode === 'rate_limited' || $this->lastStatusCode === 429;\n    }\n\n    /**\n     * isAuthError checks if the last error was an authentication error\n     */\n    public function isAuthError(): bool\n    {\n        return $this->lastErrorCode === 'invalid_auth' || $this->lastStatusCode === 401;\n    }\n\n    /**\n     * isValidationError checks if the last error was a validation error\n     */\n    public function isValidationError(): bool\n    {\n        return $this->lastErrorCode === 'validation_error' || $this->lastStatusCode === 422;\n    }\n}\n"
  },
  {
    "path": "src/Installer/InstallEventHandler.php",
    "content": "<?php\n\nnamespace October\\Rain\\Installer;\n\nuse System;\nuse System\\Models\\Parameter;\n\n/**\n * InstallEventHandler is reversed for later use\n */\nclass InstallEventHandler\n{\n    /**\n     * subscribe\n     */\n    public function subscribe($events)\n    {\n        $events->listen('backend.page.beforeDisplay', [static::class, 'extendPageDisplay']);\n    }\n\n    /**\n     * extendPageDisplay\n     */\n    public function extendPageDisplay($controller, $action, $params)\n    {\n        if (System::checkProjectValid(1|32)) {\n            $controller->addJs('/modules/backend/assets/js/onboarding.js');\n        }\n\n        if (mt_rand(1, 64) === 1) {\n            $this->checkProjectState();\n        }\n    }\n\n    /**\n     * checkProjectState\n     */\n    protected function checkProjectState()\n    {\n        return ($since = Parameter::getDate('system::core.since')) && $since->addMonths(3)->isPast()\n            ? Parameter::set('system::project.is_stale', true)\n            : Parameter::setDate('system::core.since');\n    }\n}\n"
  },
  {
    "path": "src/Installer/InstallManager.php",
    "content": "<?php\n\nnamespace October\\Rain\\Installer;\n\nuse App;\nuse Url;\nuse Lang;\nuse Http;\nuse Config;\nuse ApplicationException;\nuse Exception;\n\n/**\n * InstallManager\n *\n * @package october\\system\n * @author Alexey Bobkov, Samuel Georges\n */\nclass InstallManager\n{\n    /**\n     * @var string WANT_VERSION is the default composer version string to use.\n     */\n    const WANT_VERSION = '^4.2';\n\n    /**\n     * instance creates a new instance of this singleton\n     */\n    public static function instance(): static\n    {\n        return App::make('october.installer');\n    }\n\n    /**\n     * requestProjectDetails about a project based on its identifier.\n     * @param  string $projectId\n     * @return array\n     */\n    public function requestProjectDetails($projectId)\n    {\n        return $this->requestServerData('project/detail', ['id' => $projectId]);\n    }\n\n    /**\n     * getComposerUrl returns the endpoint for composer\n     */\n    public function getComposerUrl(bool $withProtocol = true): string\n    {\n        $gateway = env('APP_COMPOSER_GATEWAY', Config::get('system.composer_gateway', 'gateway.octobercms.com'));\n\n        return $withProtocol ? 'https://'.$gateway : $gateway;\n    }\n\n    /**\n     * requestServerData contacts the update server for a response.\n     * @param  string $uri\n     * @param  array  $postData\n     * @return array\n     */\n    public function requestServerData($uri, $postData = [])\n    {\n        $result = $this->makeHttpRequest($this->createServerUrl($uri), $postData);\n        $contents = $result->body();\n\n        if ($result->status() === 404) {\n            throw new ApplicationException(Lang::get('system::lang.server.response_not_found'));\n        }\n\n        if ($result->status() !== 200) {\n            throw new ApplicationException(\n                strlen($contents)\n                ? $contents\n                : Lang::get('system::lang.server.response_empty')\n            );\n        }\n\n        $resultData = false;\n\n        try {\n            $resultData = @json_decode($contents, true);\n        }\n        catch (Exception $ex) {\n            throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));\n        }\n\n        if ($resultData === false || (is_string($resultData) && !strlen($resultData))) {\n            throw new ApplicationException(Lang::get('system::lang.server.response_invalid'));\n        }\n\n        return $resultData;\n    }\n\n    /**\n     * createServerUrl creates a complete gateway server URL from supplied URI\n     * @param  string $uri\n     * @return string\n     */\n    protected function createServerUrl($uri)\n    {\n        $gateway = Config::get('system.update_gateway', 'https://gateway.octobercms.com/api');\n        if (substr($gateway, -1) != '/') {\n            $gateway .= '/';\n        }\n\n        return $gateway . $uri;\n    }\n\n    /**\n     * makeHttpRequest makes a specialized server request to a URL.\n     * @param string $url\n     * @param array $postData\n     * @return \\Illuminate\\Http\\Client\\Response\n     */\n    protected function makeHttpRequest($url, $postData)\n    {\n        // New HTTP instance\n        $http = Http::asForm();\n\n        // Post data\n        $postData['protocol_version'] = '2.0';\n        $postData['client'] = 'October CMS';\n        $postData['server'] = base64_encode(json_encode([\n            'php' => PHP_VERSION,\n            'url' => Url::to('/'),\n            'since' => date('c')\n        ]));\n\n        // Gateway auth\n        if ($credentials = Config::get('system.update_gateway_auth')) {\n            if (is_string($credentials)) {\n                $credentials = explode(':', $credentials);\n            }\n\n            list($user, $pass) = $credentials;\n            $http->withBasicAuth($user, $pass);\n        }\n\n        return $http->post($url, $postData);\n    }\n}\n"
  },
  {
    "path": "src/Installer/Traits/SetupBuilder.php",
    "content": "<?php\n\nnamespace October\\Rain\\Installer\\Traits;\n\nuse Lang;\nuse Exception;\nuse October\\Rain\\Installer\\InstallManager;\nuse October\\Rain\\Composer\\ComposerManager;\n\n/**\n * SetupBuilder is shared logic for the commands\n */\ntrait SetupBuilder\n{\n    /**\n     * getUpdateWantVersion\n     */\n    public function getUpdateWantVersion()\n    {\n        return \\October\\Rain\\Installer\\InstallManager::WANT_VERSION;\n    }\n\n    /**\n     * getLang\n     */\n    public function getLang($key, $vars = [])\n    {\n        return Lang::get($key, $vars);\n    }\n\n    /**\n     * setupInstallOctober installs October CMS using composer\n     */\n    protected function setupInstallOctober()\n    {\n        $composer = ComposerManager::instance();\n        $composer->setOutputCommand($this, $this->input);\n\n        $this->composerRequireCore($composer, $this->option('want') ?: null);\n        $this->line('');\n    }\n\n    /**\n     * composerRequireString returns the composer require string for installing dependencies\n     */\n    protected function composerRequireCore($composer, $want = null)\n    {\n        if ($want === null) {\n            $composer->require(['october/all' => $this->getUpdateWantVersion()]);\n        }\n        else {\n            $want = $this->processWantString($want);\n            $composer->require([\n                'october/rain' => $want,\n                'october/all' => $want\n            ]);\n        }\n    }\n\n    /**\n     * setupSetProject\n     */\n    protected function setupSetProject($licenseKey)\n    {\n        $result = InstallManager::instance()->requestProjectDetails($licenseKey);\n\n        // Check status\n        $isActive = $result['is_active'] ?? false;\n        if (!$isActive) {\n            // License is unpaid or has expired. Please visit octobercms.com to obtain a license.\n            throw new Exception(Lang::get('system::lang.installer.license_expired_comment'));\n        }\n\n        // Configure composer and save authentication token\n        $this->setComposerAuth(\n            $result['email'] ?? null,\n            $result['project_id'] ?? null\n        );\n    }\n\n    /**\n     * outputIntro displays the introduction output\n     */\n    protected function outputIntro()\n    {\n        $message = [\n            \".=================================================.\",\n            \"   ____   _____ _______  ____  ____  ___________   \",\n            \"  / __ \\ / ____|__   __|/ __ \\|  _ \\|  ____|  __ \\ \",\n            \" | |  | | |       | |  | |  | | |_) | |__  | |__) |\",\n            \" | |  | | |       | |  | |  | |  _ <|  __| |  _  / \",\n            \" | |__| | |____   | |  | |__| | |_) | |____| | \\ \\ \",\n            \"  \\____/ \\_____|  |_|   \\____/|____/|______|_|  \\_\\\\\",\n            \"                                                    \",\n            \"`================== INSTALLATION =================' \",\n            \"\",\n        ];\n\n        $this->line($message);\n    }\n\n    /**\n     * outputOutro displays the credits output\n     */\n    protected function outputOutro()\n    {\n        $message = [\n            \".~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~.\",\n            \"                ,@@@@@@@,                  \",\n            \"        ,,,.   ,@@@@@@/@@,  .oo8888o.      \",\n            \"     ,&%%&%&&%,@@@@@/@@@@@@,8888\\88/8o     \",\n            \"    ,%&\\%&&%&&%,@@@\\@@@/@@@88\\88888/88'    \",\n            \"    %&&%&%&/%&&%@@\\@@/ /@@@88888\\88888'    \",\n            \"    %&&%/ %&%%&&@@\\ V /@@' `88\\8 `/88'     \",\n            \"    `&%\\ ` /%&'    |.|        \\ '|8'       \",\n            \"        |o|        | |         | |         \",\n            \"        |.|        | |         | |         \",\n            \"`========= INSTALLATION COMPLETE ========='\",\n            \"\",\n        ];\n\n        $this->line($message);\n\n        // Please migrate the database with the following command\n        $this->comment(Lang::get('system::lang.installer.migrate_database_comment'));\n        $this->line('');\n        $this->line(\"* php artisan october:migrate\");\n        $this->line('');\n\n        $adminUrl = $this->getEnvVar('APP_URL') . $this->getEnvVar('BACKEND_URI');\n        // Then, open the administration area at this URL\n        $this->comment(Lang::get('system::lang.installer.visit_backend_comment'));\n        $this->line('');\n        $this->line(\"* {$adminUrl}\");\n    }\n\n    /**\n     * outputFailedOutro displays the failure message\n     */\n    protected function outputFailedOutro()\n    {\n        // Installation Failed\n        $this->output->title(Lang::get('system::lang.installer.install_failed_label'));\n\n        // Please try running these commands manually.\n        $this->output->error(Lang::get('system::lang.installer.install_failed_comment'));\n        $this->line('');\n\n        // Open this application in your browser\n        $this->line(Lang::get('system::lang.installer.open_configurator_comment'));\n        $this->line('');\n\n        $this->line('-- OR --');\n        $this->line('');\n\n        $this->line(\"* php artisan project:set <LICENSE KEY>\");\n        $this->line('');\n\n        if ($want = $this->option('want')) {\n            $this->line(\"* php artisan october:build --want=\".$want);\n        }\n        else {\n            $this->line(\"* php artisan october:build\");\n        }\n    }\n\n    /**\n     * nonInteractiveCheck will make a calculated guess if the command is running\n     * in non interactive mode by how long it takes to execute\n     */\n    protected function nonInteractiveCheck(): bool\n    {\n        return (microtime(true) - LARAVEL_START) < 1;\n    }\n}\n"
  },
  {
    "path": "src/Installer/Traits/SetupHelper.php",
    "content": "<?php\n\nnamespace October\\Rain\\Installer\\Traits;\n\nuse App;\nuse Str;\nuse Config;\nuse October\\Rain\\Installer\\InstallManager;\nuse October\\Rain\\Composer\\ComposerManager;\nuse Illuminate\\Support\\Env;\nuse Dotenv\\Dotenv;\nuse Exception;\nuse PDOException;\nuse PDO;\n\n/**\n * SetupHelper is shared logic for the handlers\n */\ntrait SetupHelper\n{\n    /**\n     * @var array userConfig is a temporary store of user input config values\n     */\n    protected $userConfig = [];\n\n    /**\n     * setComposerAuth configures authentication for composer and October CMS\n     */\n    protected function setComposerAuth($email, $projectKey)\n    {\n        $composer = ComposerManager::instance();\n\n        // Save authentication token\n        $composer->addAuthCredentials(\n            $this->getComposerUrl(false),\n            $email,\n            $projectKey\n        );\n\n        // Store project details\n        $this->injectJsonToFile(storage_path('cms/project.json'), [\n            'project' => $projectKey\n        ]);\n\n        // Add gateway as a composer repo\n        $composer->addOctoberRepository($this->getComposerUrl());\n    }\n\n    /**\n     * setDemoContent instructs the system to install demo content or not\n     */\n    protected function setDemoContent($confirm = true)\n    {\n        if ($confirm) {\n            $this->injectJsonToFile(storage_path('cms/autoexec.json'), [\n                'theme:seed demo --root'\n            ]);\n        }\n        else {\n            $this->injectJsonToFile(storage_path('cms/autoexec.json'), [\n                'october:fresh --force'\n            ]);\n        }\n    }\n\n    /**\n     * processWantString ensures a valid want version is supplied\n     */\n    protected function processWantString($version)\n    {\n        $parts = explode('.', $version);\n\n        if (count($parts) > 1) {\n            $parts[2] = '*';\n        }\n\n        $parts = array_slice($parts, 0, 3);\n\n        return implode('.', $parts);\n    }\n\n    /**\n     * addModulesToGitignore\n     */\n    protected function addModulesToGitignore($gitignore)\n    {\n        $toIgnore = '/modules';\n        $contents = file_get_contents($gitignore);\n\n        if (!preg_match('/^\\/modules$/m', $contents)) {\n            file_put_contents(\n                $gitignore,\n                trim($contents, PHP_EOL) .\n                PHP_EOL .\n                $toIgnore .\n                PHP_EOL\n            );\n        }\n    }\n\n    /**\n     * setEnvVars sets multiple environment variables\n     */\n    protected function setEnvVars(array $vars)\n    {\n        foreach ($vars as $key => $val) {\n            $this->setEnvVar($key, $val);\n        }\n    }\n\n    /**\n     * setEnvVar writes an environment variable to disk\n     */\n    protected function setEnvVar($key, $value)\n    {\n        $path = base_path('.env');\n        $old = $this->getEnvVar($key);\n        $value = $this->encodeEnvVar($value);\n\n        if (is_bool(env($key))) {\n            $old = env($key) ? 'true' : 'false';\n        }\n\n        if (file_exists($path)) {\n            file_put_contents($path, str_replace(\n                [$key.'='.$old, $key.'='.'\"'.$old.'\"'],\n                [$key.'='.$value, $key.'='.$value],\n                file_get_contents($path)\n            ));\n        }\n\n        $this->userConfig[$key] = $value;\n    }\n\n    /**\n     * encodeEnvVar for compatibility with certain characters\n     */\n    protected function encodeEnvVar($value)\n    {\n        if (!is_string($value)) {\n            return $value;\n        }\n\n        // Escape quotes\n        if (strpos($value, '\"') !== false) {\n            $value = str_replace('\"', '\\\"', $value);\n        }\n\n        // Quote values with comment, space, quotes\n        $triggerChars = ['#', ' ', '\"', \"'\"];\n        foreach ($triggerChars as $char) {\n            if (strpos($value, $char) !== false) {\n                $value = '\"'.$value.'\"';\n                break;\n            }\n        }\n\n        return $value;\n    }\n\n    /**\n     * getEnvVar specifically from installer specified values. This is needed since\n     * the writing to the environment file may not update the values from env()\n     */\n    protected function getEnvVar(string $key, $default = null)\n    {\n        return $this->userConfig[$key] ?? env($key, $default);\n    }\n\n    /**\n     * checkDatabase validates the supplied database configuration\n     */\n    protected function checkDatabase($type, $host, $port, $name, $user, $pass)\n    {\n        if ($type != 'sqlite' && !strlen($host)) {\n            throw new Exception('Please specify a database host');\n        }\n\n        if (!strlen($name)) {\n            throw new Exception('Please specify the database name');\n        }\n\n        // Check connection\n        switch ($type) {\n            case 'mysql':\n                $dsn = 'mysql:host='.$host.';dbname='.$name;\n                if ($port) {\n                    $dsn .= \";port=\".$port;\n                }\n                break;\n\n            case 'pgsql':\n                $_host = ($host) ? 'host='.$host.';' : '';\n                $dsn = 'pgsql:'.$_host.'dbname='.$name;\n                if ($port) {\n                    $dsn .= \";port=\".$port;\n                }\n                break;\n\n            case 'sqlite':\n                $dsn = 'sqlite:'.$name;\n                $this->checkSqliteFile($name);\n                break;\n\n            case 'sqlsrv':\n                $availableDrivers = PDO::getAvailableDrivers();\n                $portStr = $port ? ','.$port : '';\n                if (in_array('dblib', $availableDrivers)) {\n                    $dsn = 'dblib:host='.$host.$portStr.';dbname='.$name;\n                }\n                else {\n                    $dsn = 'sqlsrv:Server='.$host.$portStr.';Database='.$name;\n                }\n                break;\n        }\n        try {\n            return new PDO($dsn, $user, $pass, array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION));\n        }\n        catch (PDOException $ex) {\n            throw new Exception('Connection failed: ' . $ex->getMessage());\n        }\n    }\n\n    /**\n     * validateSqliteFile will establish the SQLite engine\n     */\n    protected function checkSqliteFile($filename)\n    {\n        if (file_exists($filename)) {\n            return;\n        }\n\n        $directory = dirname($filename);\n        if (!is_dir($directory)) {\n            mkdir($directory, 0777, true);\n        }\n\n        new PDO('sqlite:'.$filename);\n    }\n\n    /**\n     * injectJsonToFile merges a JSON array in to an existing JSON file.\n     * Merging is useful for preserving array values.\n     */\n    protected function injectJsonToFile(string $filename, array $jsonArr, bool $merge = false): void\n    {\n        $contentsArr = file_exists($filename)\n            ? json_decode(file_get_contents($filename), true)\n            : [];\n\n        $newArr = $merge\n            ? array_merge_recursive($contentsArr, $jsonArr)\n            : $this->mergeRecursive($contentsArr, $jsonArr);\n\n        $content = json_encode($newArr, JSON_UNESCAPED_SLASHES|JSON_PRETTY_PRINT);\n\n        file_put_contents($filename, $content);\n    }\n\n    /**\n     * mergeRecursive substitutes the native PHP array_merge_recursive to be\n     * more config friendly. Scalar values are replaced instead of being\n     * merged in to their own new array.\n     */\n    protected function mergeRecursive(array $array1, $array2)\n    {\n        if ($array2 && is_array($array2)) {\n            foreach ($array2 as $key => $val2) {\n                if (\n                    is_array($val2) &&\n                    (($val1 = isset($array1[$key]) ? $array1[$key] : null) !== null) &&\n                    is_array($val1)\n                ) {\n                    $array1[$key] = $this->mergeRecursive($val1, $val2);\n                }\n                else {\n                    $array1[$key] = $val2;\n                }\n            }\n        }\n\n        return $array1;\n    }\n\n    /**\n     * getAvailableLocales returns available system locales\n     */\n    public function getAvailableLocales()\n    {\n        return [\n            'ar'    => [$this->getLang('system::lang.locale.ar'),    'Arabic'],\n            'be'    => [$this->getLang('system::lang.locale.be'),    'Belarusian'],\n            'bg'    => [$this->getLang('system::lang.locale.bg'),    'Bulgarian'],\n            'ca'    => [$this->getLang('system::lang.locale.ca'),    'Catalan'],\n            'cs'    => [$this->getLang('system::lang.locale.cs'),    'Czech'],\n            'da'    => [$this->getLang('system::lang.locale.da'),    'Danish'],\n            'de'    => [$this->getLang('system::lang.locale.de'),    'German'],\n            'el'    => [$this->getLang('system::lang.locale.el'),    'Greek'],\n            'en'    => [$this->getLang('system::lang.locale.en'),    'English'],\n            'en-au' => [$this->getLang('system::lang.locale.en-au'), 'English'],\n            'en-ca' => [$this->getLang('system::lang.locale.en-ca'), 'English'],\n            'en-gb' => [$this->getLang('system::lang.locale.en-gb'), 'English'],\n            'es'    => [$this->getLang('system::lang.locale.es'),    'Spanish'],\n            'es-ar' => [$this->getLang('system::lang.locale.es-ar'), 'Spanish'],\n            'et'    => [$this->getLang('system::lang.locale.et'),    'Estonian'],\n            'fa'    => [$this->getLang('system::lang.locale.fa'),    'Persian'],\n            'fi'    => [$this->getLang('system::lang.locale.fi'),    'Finnish'],\n            'fr'    => [$this->getLang('system::lang.locale.fr'),    'French'],\n            'fr-ca' => [$this->getLang('system::lang.locale.fr-ca'), 'French'],\n            'hu'    => [$this->getLang('system::lang.locale.hu'),    'Hungarian'],\n            'id'    => [$this->getLang('system::lang.locale.id'),    'Indonesian'],\n            'it'    => [$this->getLang('system::lang.locale.it'),    'Italian'],\n            'ja'    => [$this->getLang('system::lang.locale.ja'),    'Japanese'],\n            'ko'    => [$this->getLang('system::lang.locale.ko'),    'Korean'],\n            'lt'    => [$this->getLang('system::lang.locale.lt'),    'Lithuanian'],\n            'lv'    => [$this->getLang('system::lang.locale.lv'),    'Latvian'],\n            'nb-no' => [$this->getLang('system::lang.locale.nb-no'), 'Norwegian'],\n            'nl'    => [$this->getLang('system::lang.locale.nl'),    'Dutch'],\n            'pl'    => [$this->getLang('system::lang.locale.pl'),    'Polish'],\n            'pt-br' => [$this->getLang('system::lang.locale.pt-br'), 'Portuguese'],\n            'pt-pt' => [$this->getLang('system::lang.locale.pt-pt'), 'Portuguese'],\n            'ro'    => [$this->getLang('system::lang.locale.ro'),    'Romanian'],\n            'ru'    => [$this->getLang('system::lang.locale.ru'),    'Russian'],\n            'sk'    => [$this->getLang('system::lang.locale.sk'),    'Slovak'],\n            'sl'    => [$this->getLang('system::lang.locale.sl'),    'Slovene'],\n            'sv'    => [$this->getLang('system::lang.locale.sv'),    'Swedish'],\n            'th'    => [$this->getLang('system::lang.locale.th'),    'Thai'],\n            'tr'    => [$this->getLang('system::lang.locale.tr'),    'Turkish'],\n            'uk'    => [$this->getLang('system::lang.locale.uk'),    'Ukrainian'],\n            'vn'    => [$this->getLang('system::lang.locale.vn'),    'Vietnamese'],\n            'zh-cn' => [$this->getLang('system::lang.locale.zh-cn'), 'Chinese'],\n            'zh-tw' => [$this->getLang('system::lang.locale.zh-tw'), 'Chinese'],\n        ];\n    }\n\n    //\n    // Framework Booted\n    //\n\n    /**\n     * getRandomKey generates a random application key\n     */\n    protected function getRandomKey(): string\n    {\n        return Str::random($this->getKeyLength(Config::get('app.cipher')));\n    }\n\n    /**\n     * getKeyLength returns the supported length of a key for a cipher\n     */\n    protected function getKeyLength(string $cipher): int\n    {\n        return $cipher === 'AES-128-CBC' ? 16 : 32;\n    }\n\n    /**\n     * checkEnvWritable checks to see if the app can write to the .env file\n     */\n    protected function checkEnvWritable()\n    {\n        $path = base_path('.env');\n        $gitignore = base_path('.gitignore');\n\n        // Copy environment variables and reload\n        if (!file_exists($path)) {\n            copy(base_path('.env.example'), $path);\n            $this->refreshEnvVars();\n        }\n\n        // Add modules to .gitignore\n        if (file_exists($gitignore) && is_writable($gitignore)) {\n            $this->addModulesToGitignore($gitignore);\n        }\n\n        return is_writable($path);\n    }\n\n    /**\n     * getComposerUrl returns the endpoint for composer\n     */\n    protected function getComposerUrl(bool $withProtocol = true): string\n    {\n        return InstallManager::instance()->getComposerUrl($withProtocol);\n    }\n\n    /**\n     * refreshEnvVars will reload defined environment variables\n     */\n    protected function refreshEnvVars()\n    {\n        DotEnv::create(Env::getRepository(), App::environmentPath(), App::environmentFile())->load();\n    }\n}\n"
  },
  {
    "path": "src/Mail/FakeMailer.php",
    "content": "<?php namespace October\\Rain\\Mail;\n\nuse October\\Rain\\Mail\\Mailable;\nuse Illuminate\\Support\\Collection;\nuse Illuminate\\Support\\Testing\\Fakes\\MailFake as MailFakeBase;\n\n/**\n * FakeMailer\n *\n * @package october\\mail\n * @author Alexey Bobkov, Samuel Georges, Alexander Guth\n */\nclass FakeMailer extends MailFakeBase\n{\n    /**\n     * send a new message using a view\n     */\n    public function send($view, $data = [], $callback = null): void\n    {\n        if (!$view instanceof Mailable) {\n            $view = $this->buildMailable($view, $data, $callback);\n        }\n\n        parent::send($view, $data = [], $callback = null);\n    }\n\n    /**\n     * queue a new e-mail message for sending\n     */\n    public function queue($view, $data = null, $callback = null, $queue = null)\n    {\n        if (!$view instanceof Mailable) {\n            $view = $this->buildMailable($view, $data, $callback, true);\n        }\n\n        return parent::queue($view, $data = null, $callback = null, $queue = null);\n    }\n\n    /**\n     * mailablesOf a given type\n     */\n    protected function mailablesOf($type): Collection\n    {\n        return collect($this->mailables)->filter(function ($mailable) use ($type) {\n            return $mailable->view === $type;\n        });\n    }\n\n    /**\n     * queuedMailablesOf of a given type\n     */\n    protected function queuedMailablesOf($type): Collection\n    {\n        return collect($this->queuedMailables)->filter(function ($mailable) use ($type) {\n            return $mailable->view === $type;\n        });\n    }\n\n    /**\n     * buildMailable from a view file\n     */\n    public function buildMailable($view, $data, $callback, $queued = false)\n    {\n        $mailable = new Mailable;\n\n        $mailable->locale('en');\n\n        $mailable->siteContext(1);\n\n        if ($queued) {\n            $mailable->view($view)->withSerializedData($data);\n        }\n        else {\n            $mailable->view($view, $data);\n        }\n\n        if ($callback !== null) {\n            call_user_func($callback, $mailable);\n        }\n\n        return $mailable;\n    }\n}\n"
  },
  {
    "path": "src/Mail/MailManager.php",
    "content": "<?php namespace October\\Rain\\Mail;\n\nuse Illuminate\\Mail\\MailManager as MailManagerBase;\nuse InvalidArgumentException;\n\n/**\n * MailManager class for sending mail.\n *\n * @package october\\mail\n * @author Alexey Bobkov, Samuel Georges\n */\nclass MailManager extends MailManagerBase\n{\n    /**\n     * resolve the given mailer. Copy of parent method, replacing Mailer class.\n     * @param  string  $name\n     * @return \\Illuminate\\Mail\\Mailer\n     * @throws \\InvalidArgumentException\n     */\n    protected function resolve($name)\n    {\n        // Extensibility\n        $this->app['events']->dispatch('mailer.beforeResolve', [$this, $name]);\n\n        $config = $this->getConfig($name);\n\n        if (is_null($config)) {\n            throw new InvalidArgumentException(\"Mailer [{$name}] is not defined.\");\n        }\n\n        // Once we have created the mailer instance we will set a container instance\n        // on the mailer. This allows us to resolve mailer classes via containers\n        // for maximum testability on said classes instead of passing Closures.\n        $mailer = new Mailer(\n            $name,\n            $this->app['view'],\n            $this->createSymfonyTransport($config),\n            $this->app['events']\n        );\n\n        if ($this->app->bound('queue')) {\n            $mailer->setQueue($this->app['queue']);\n        }\n\n        // Next we will set all of the global addresses on this mailer, which allows\n        // for easy unification of all \"from\" addresses as well as easy debugging\n        // of sent messages since these will be sent to a single email address.\n        foreach (['from', 'reply_to', 'to', 'return_path'] as $type) {\n            $this->setGlobalAddress($mailer, $config, $type);\n        }\n\n        // Extensibility\n        $this->app['events']->dispatch('mailer.resolve', [$this, $name, $mailer]);\n\n        return $mailer;\n    }\n}\n"
  },
  {
    "path": "src/Mail/MailParser.php",
    "content": "<?php namespace October\\Rain\\Mail;\n\nuse October\\Rain\\Support\\Facades\\Ini;\n\n/**\n * This class parses Mail templates.\n * Returns the structured file information.\n *\n * @package october\\mail\n * @author Alexey Bobkov, Samuel Georges\n */\nclass MailParser\n{\n    const SECTION_SEPARATOR = '==';\n\n    /**\n     * Parses Mail template content.\n     * The expected file format is following:\n     * <pre>\n     * Settings section\n     * ==\n     * Plain-text content section\n     * ==\n     * HTML content section\n     * ==\n     * CSS stylesheet section (layouts)\n     * </pre>\n     * If the content has only 2 sections they are considered as settings and HTML.\n     * If there is only a single section, it is considered as HTML.\n     * @param string $content Specifies the file content.\n     * @return array Returns an array with the following indexes: 'settings', 'html', 'text'.\n     * The 'html' and 'text' elements contain strings. The 'settings' element contains the\n     * parsed INI file as array. If the content string doesn't contain a section, the corresponding\n     * result element has null value.\n     */\n    public static function parse($content)\n    {\n        $sections = preg_split('/^={2,}\\s*/m', $content, -1);\n        $count = count($sections);\n        foreach ($sections as &$section) {\n            $section = trim($section);\n        }\n\n        $result = [\n            'settings' => [],\n            'html' => null,\n            'text' => null,\n            'css' => null\n        ];\n\n        if ($count >= 4) {\n            $result['settings'] = Ini::parse($sections[0]);\n            $result['text'] = $sections[1];\n            $result['html'] = $sections[2];\n            $result['css'] = $sections[3];\n        }\n        elseif ($count >= 3) {\n            $result['settings'] = Ini::parse($sections[0]);\n            $result['text'] = $sections[1];\n            $result['html'] = $sections[2];\n        }\n        elseif ($count === 2) {\n            $result['settings'] = Ini::parse($sections[0]);\n            $result['html'] = $sections[1];\n        }\n        elseif ($count === 1) {\n            $result['html'] = $sections[0];\n        }\n\n        return $result;\n    }\n}\n"
  },
  {
    "path": "src/Mail/MailServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Mail;\n\nuse Illuminate\\Mail\\MailServiceProvider as MailServiceProviderBase;\n\n/**\n * MailServiceProvider\n *\n * @package october\\mail\n * @author Alexey Bobkov, Samuel Georges\n */\nclass MailServiceProvider extends MailServiceProviderBase\n{\n    /**\n     * registerIlluminateMailer instance, as a copy of parent with extensibility.\n     */\n    protected function registerIlluminateMailer()\n    {\n        $this->app->singleton('mail.manager', function ($app) {\n            // @deprecated use mailer.beforeResolve or callBeforeResolving\n            $this->app['events']->dispatch('mailer.beforeRegister', [$this]);\n\n            // Inheritance\n            $manager = new MailManager($app);\n\n            // @deprecated use mailer.resolve or callAfterResolving\n            $this->app['events']->dispatch('mailer.register', [$this, $manager]);\n\n            return $manager;\n        });\n\n        $this->app->bind('mailer', function ($app) {\n            return $app->make('mail.manager')->mailer();\n        });\n    }\n}\n"
  },
  {
    "path": "src/Mail/Mailable.php",
    "content": "<?php namespace October\\Rain\\Mail;\n\nuse App;\nuse Site;\nuse Illuminate\\Mail\\Mailable as MailableBase;\n\n/**\n * Generic mailable class.\n *\n * @package october\\mail\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Mailable extends MailableBase\n{\n    use \\Illuminate\\Bus\\Queueable;\n    use \\Illuminate\\Queue\\SerializesModels;\n\n    /**\n     * @var string siteContext is the active site for this mail message.\n     */\n    public $siteContext;\n\n    /**\n     * @var string forceMailer forces the mailer to use.\n     */\n    public $forceMailer;\n\n    /**\n     * Build the message.\n     *\n     * @return $this\n     */\n    public function build()\n    {\n        return $this;\n    }\n\n    /**\n     * Build the view data for the message.\n     *\n     * @return array\n     */\n    public function buildViewData()\n    {\n        $data = $this->viewData;\n\n        foreach ($data as $param => $value) {\n            $data[$param] = $this->getRestoredPropertyValue($value);\n        }\n\n        return $data;\n    }\n\n    /**\n     * Set serialized view data for the message.\n     *\n     * @param  array  $data\n     * @return $this\n     */\n    public function withSerializedData($data)\n    {\n        $this->viewData['_current_locale'] = $this->locale ?: App::getLocale();\n\n        $this->viewData['_current_site'] = $this->siteContext ?: Site::getSiteIdFromContext();\n\n        foreach ($data as $param => $value) {\n            $this->viewData[$param] = $this->getSerializedPropertyValue($value);\n        }\n\n        return $this;\n    }\n\n    /**\n     * Set the subject for the message.\n     *\n     * @param  \\Illuminate\\Mail\\Message  $message\n     * @return $this\n     */\n    protected function buildSubject($message)\n    {\n        if ($this->subject) {\n            $message->subject($this->subject);\n        }\n\n        return $this;\n    }\n\n    /**\n     * siteContext sets the site context of the message.\n     *\n     * @param  string  $siteId\n     * @return $this\n     */\n    public function siteContext($siteId)\n    {\n        $this->siteContext = $siteId;\n\n        return $this;\n    }\n\n    /**\n     * withLocale acts as a hook to also apply the site context\n     *\n     * @param  string  $locale\n     * @param  \\Closure  $callback\n     * @return mixed\n     */\n    public function withLocale($locale, $callback)\n    {\n        if (!$this->siteContext) {\n            return parent::withLocale($locale, $callback);\n        }\n\n        return Site::withContext($this->siteContext, function() use ($locale, $callback) {\n            return parent::withLocale($locale, $callback);\n        });\n    }\n\n    /**\n     * forceMailer forces sending using a different mail driver, useful if lazy loading\n     * the mail driver configuration for multisite.\n     * @param  string  $mailer\n     * @return $this\n     */\n    public function forceMailer($mailer)\n    {\n        $this->forceMailer = $mailer;\n\n        return $this;\n    }\n\n    /**\n     * mailer sets the name of the mailer that should send the message.\n     * @param  string  $mailer\n     * @return $this\n     */\n    public function mailer($mailer)\n    {\n        $this->mailer = $this->forceMailer ?: $mailer;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Mail/Mailer.php",
    "content": "<?php namespace October\\Rain\\Mail;\n\nuse App;\nuse Site;\nuse Event;\nuse Config;\nuse Illuminate\\Mail\\Mailer as MailerBase;\nuse Illuminate\\Mail\\SentMessage;\nuse Illuminate\\Contracts\\Mail\\Mailable as MailableContract;\nuse Illuminate\\Support\\Arr;\nuse Illuminate\\Support\\Collection;\n\n/**\n * Mailer class for sending mail.\n *\n * @package october\\mail\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Mailer extends MailerBase\n{\n    use \\October\\Rain\\Support\\Traits\\Emitter;\n\n    /**\n     * @var string pretendingOriginal contains the original driver before pretending.\n     */\n    protected $pretendingOriginal;\n\n    /**\n     * send a new message using a view.\n     *\n     * @param  string|array $view\n     * @param  array $data\n     * @param  \\Closure|string $callback\n     * @return mixed\n     */\n    public function send($view, array $data = [], $callback = null)\n    {\n        /**\n         * @event mailer.beforeSend\n         * Fires before the mailer processes the sending action\n         *\n         * Example usage (stops the sending process):\n         *\n         *     Event::listen('mailer.beforeSend', function ((string|array) $view, (array) $data, (\\Closure|string) $callback) {\n         *         return false;\n         *     });\n         *\n         * Or\n         *\n         *     $mailerInstance->bindEvent('mailer.beforeSend', function ((string|array) $view, (array) $data, (\\Closure|string) $callback) {\n         *         return false;\n         *     });\n         *\n         */\n        if (\n            ($this->fireEvent('mailer.beforeSend', [$view, $data, $callback], true) === false) ||\n            (Event::fire('mailer.beforeSend', [$view, $data, $callback], true) === false)\n        ) {\n            return;\n        }\n\n        if ($view instanceof MailableContract) {\n            return $this->sendMailable($view);\n        }\n\n        // Inheriting logic from Illuminate\\Mail\\Mailer...\n\n        // First we need to parse the view, which could either be a string or an array\n        // containing both an HTML and plain text versions of the view which should\n        // be used when sending an e-mail. We will extract both of them out here.\n        list($view, $plain, $raw) = $this->parseView($view);\n\n        $data['message'] = $message = $this->createMessage();\n\n        if (!is_null($callback)) {\n            $callback($message);\n        }\n\n        if (is_bool($raw) && $raw === true) {\n            $this->addContentRaw($message, $view, $plain);\n        }\n        else {\n            $this->addContent($message, $view, $plain, $raw, $data);\n        }\n\n        // If a global \"to\" address has been set, we will set that address on the mail\n        // message. This is primarily useful during local development in which each\n        // message should be delivered into a single mail address for inspection.\n        if (isset($this->to['address'])) {\n            $this->setGlobalToAndRemoveCcAndBcc($message);\n        }\n\n        /**\n         * @event mailer.prepareSend\n         * Fires before the mailer processes the sending action\n         *\n         * Parameters:\n         * - $view: View code as a string\n         * - $message: Illuminate\\Mail\\Message object, check Swift_Mime_SimpleMessage for useful functions.\n         *\n         * Example usage (stops the sending process):\n         *\n         *     Event::listen('mailer.prepareSend', function ((\\October\\Rain\\Mail\\Mailer) $mailerInstance, (string) $view, (\\Illuminate\\Mail\\Message) $message) {\n         *         return false;\n         *     });\n         *\n         * Or\n         *\n         *     $mailerInstance->bindEvent('mailer.prepareSend', function ((string) $view, (\\Illuminate\\Mail\\Message) $message) {\n         *         return false;\n         *     });\n         *\n         */\n        if (\n            ($this->fireEvent('mailer.prepareSend', [$view, $message], true) === false) ||\n            (Event::fire('mailer.prepareSend', [$this, $view, $message], true) === false)\n        ) {\n            return;\n        }\n\n        // Next we will determine if the message should be sent. We give the developer\n        // one final chance to stop this message and then we will send it to all of\n        // its recipients. We will then fire the sent event for the sent message.\n        $symfonyMessage = $message->getSymfonyMessage();\n\n        if ($this->shouldSendMessage($symfonyMessage, $data)) {\n            $symfonySentMessage = $this->sendSymfonyMessage($symfonyMessage);\n\n            if ($symfonySentMessage) {\n                $sentMessage = new SentMessage($symfonySentMessage);\n\n                $this->dispatchSentEvent($sentMessage, $data);\n\n                /**\n                 * @event mailer.send\n                 * Fires after the message has been sent\n                 *\n                 * Example usage (logs the message):\n                 *\n                 *     Event::listen('mailer.send', function ((\\October\\Rain\\Mail\\Mailer) $mailerInstance, (string) $view, (\\Illuminate\\Mail\\Message) $message) {\n                 *         \\Log::info(\"Message was rendered with $view and sent\");\n                 *     });\n                 *\n                 * Or\n                 *\n                 *     $mailerInstance->bindEvent('mailer.send', function ((string) $view, (\\Illuminate\\Mail\\Message) $message) {\n                 *         \\Log::info(\"Message was rendered with $view and sent\");\n                 *     });\n                 *\n                 */\n                $this->fireEvent('mailer.send', [$view, $message]);\n                Event::fire('mailer.send', [$this, $view, $message]);\n\n                return $sentMessage;\n            }\n        }\n    }\n\n    /**\n     * sendTo is a helper for send() method, the first argument can take a single email or an\n     * array of recipients where the key is the address and the value is the name.\n     *\n     * @param  array $recipients\n     * @param  string|array $view\n     * @param  array $data\n     * @param  mixed $callback\n     * @param  array $options\n     * @return void\n     */\n    public function sendTo($recipients, $view, array $data = [], $callback = null, $options = [])\n    {\n        if ($callback && !$options && !is_callable($callback)) {\n            $options = $callback;\n        }\n\n        if (is_bool($options)) {\n            $queue = $options;\n            $bcc = false;\n        }\n        else {\n            extract(array_merge([\n                'queue' => false,\n                'bcc' => false\n            ], $options));\n        }\n\n        $method = $queue === true ? 'queue' : 'send';\n        $recipients = $this->processRecipients($recipients);\n\n        return $this->{$method}($view, $data, function ($message) use ($recipients, $callback, $bcc) {\n            $method = $bcc === true ? 'bcc' : 'to';\n            foreach ($recipients as $address => $name) {\n                $message->{$method}($address, $name);\n            }\n\n            if (is_callable($callback)) {\n                $callback($message);\n            }\n        });\n    }\n\n    /**\n     * queue a new e-mail message for sending.\n     *\n     * @param  string|array  $view\n     * @param  array  $data\n     * @param  \\Closure|string  $callback\n     * @param  string|null  $queue\n     * @return mixed\n     */\n    public function queue($view, $data = null, $callback = null, $queue = null)\n    {\n        if (!$view instanceof MailableContract) {\n            $mailable = $this->buildQueueMailable($view, $data, $callback, $queue);\n            $queue = null;\n        }\n        else {\n            $mailable = $view;\n            $queue = $queue ?? $data;\n        }\n\n        return parent::queue($mailable, $queue);\n    }\n\n    /**\n     * queueOn queues a new e-mail message for sending on the given queue.\n     *\n     * @param  string  $queue\n     * @param  string|array  $view\n     * @param  array  $data\n     * @param  \\Closure|string  $callback\n     * @return mixed\n     */\n    public function queueOn($queue, $view, $data = null, $callback = null)\n    {\n        return $this->queue($view, $data, $callback, $queue);\n    }\n\n    /**\n     * later queues a new e-mail message for sending after (n) seconds.\n     *\n     * @param  int  $delay\n     * @param  string|array  $view\n     * @param  array  $data\n     * @param  \\Closure|string  $callback\n     * @param  string|null  $queue\n     * @return mixed\n     */\n    public function later($delay, $view, $data = null, $callback = null, $queue = null)\n    {\n        if (!$view instanceof MailableContract) {\n            $mailable = $this->buildQueueMailable($view, $data, $callback, $queue);\n            $queue = null;\n        }\n        else {\n            $mailable = $view;\n            $queue = $queue ?? $data;\n        }\n\n        return parent::later($delay, $mailable, $queue);\n    }\n\n    /**\n     * laterOn queues a new e-mail message for sending after (n) seconds on the given queue.\n     *\n     * @param  string  $queue\n     * @param  int  $delay\n     * @param  string|array  $view\n     * @param  array  $data\n     * @param  \\Closure|string  $callback\n     * @return mixed\n     */\n    public function laterOn($queue, $delay, $view, ?array $data = null, $callback = null)\n    {\n        return $this->later($delay, $view, $data, $callback, $queue);\n    }\n\n    /**\n     * buildQueueMailable for a queued email job.\n     *\n     * @param  mixed  $callback\n     * @return mixed\n     */\n    protected function buildQueueMailable($view, $data, $callback, $queue)\n    {\n        $mailable = new Mailable;\n\n        $mailable->locale(App::getLocale());\n\n        $mailable->siteContext(Site::getSiteIdFromContext());\n\n        $mailable->view($view)->withSerializedData($data);\n\n        if ($queue !== null) {\n            $mailable->onQueue($queue);\n        }\n\n        if ($callback !== null) {\n            call_user_func($callback, $mailable);\n        }\n\n        /**\n         * @event mailer.buildQueueMailable\n         * Process the mailable object used when adding mail to the queue\n         *\n         * Example usage:\n         *\n         *     Event::listen('mailer.buildQueueMailable', function ((\\October\\Rain\\Mail\\Mailer) $mailerInstance, (\\October\\Rain\\Mail\\Mailable) $mailable) {\n         *         $mailable->mailer('smtp');\n         *     });\n         *\n         */\n        $this->fireEvent('mailer.buildQueueMailable', [$mailable]);\n        Event::fire('mailer.buildQueueMailable', [$this, $mailable]);\n\n        return $mailable;\n    }\n\n    /**\n     * raw sends a new message when only a raw text part.\n     *\n     * @param  string  $text\n     * @param  mixed  $callback\n     * @return int\n     */\n    public function raw($view, $callback)\n    {\n        if (!is_array($view)) {\n            $view = ['raw' => $view];\n        }\n        elseif (!array_key_exists('raw', $view)) {\n            $view['raw'] = true;\n        }\n\n        return $this->send($view, [], $callback);\n    }\n\n    /**\n     * rawTo helper for raw() method, send a new message when only a raw text part.\n     *\n     * @param  array $recipients\n     * @param  string  $view\n     * @param  mixed   $callback\n     * @param  array   $options\n     * @return int\n     */\n    public function rawTo($recipients, $view, $callback = null, $options = [])\n    {\n        if (!is_array($view)) {\n            $view = ['raw' => $view];\n        }\n        elseif (!array_key_exists('raw', $view)) {\n            $view['raw'] = true;\n        }\n\n        return $this->sendTo($recipients, $view, [], $callback, $options);\n    }\n\n    /**\n     * processRecipients object, which can look like the following:\n     *  - (string) admin@domain.tld\n     *  - (object) ['email' => 'admin@domain.tld', 'name' => 'Adam Person']\n     *  - (array) ['admin@domain.tld' => 'Adam Person', ...]\n     *  - (array) [ (object|array) ['email' => 'admin@domain.tld', 'name' => 'Adam Person'], [...] ]\n     * @param mixed $recipients\n     * @return array\n     */\n    protected function processRecipients($recipients)\n    {\n        $result = [];\n\n        if (is_string($recipients)) {\n            $result[$recipients] = null;\n        }\n        elseif (is_array($recipients) || $recipients instanceof Collection) {\n            foreach ($recipients as $address => $person) {\n                if (is_string($person)) {\n                    $result[$address] = $person;\n                }\n                elseif (is_object($person)) {\n                    if (empty($person->email) && empty($person->address)) {\n                        continue;\n                    }\n\n                    $address = !empty($person->email) ? $person->email : $person->address;\n                    $name = !empty($person->name) ? $person->name : null;\n                    $result[$address] = $name;\n                }\n                elseif (is_array($person)) {\n                    if (!$address = Arr::get($person, 'email', Arr::get($person, 'address'))) {\n                        continue;\n                    }\n\n                    $result[$address] = Arr::get($person, 'name');\n                }\n            }\n        }\n        elseif (is_object($recipients)) {\n            if (!empty($recipients->email) || !empty($recipients->address)) {\n                $address = !empty($recipients->email) ? $recipients->email : $recipients->address;\n                $name = !empty($recipients->name) ? $recipients->name : null;\n                $result[$address] = $name;\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * addContent to a given message.\n     *\n     * @param  \\Illuminate\\Mail\\Message $message\n     * @param  string $view\n     * @param  string $plain\n     * @param  string $raw\n     * @param  array $data\n     * @return void\n     */\n    protected function addContent($message, $view, $plain, $raw, $data)\n    {\n        /**\n         * @event mailer.beforeAddContent\n         * Fires before the mailer adds content to the message\n         *\n         * Example usage (stops the content adding process):\n         *\n         *     Event::listen('mailer.beforeAddContent', function ((\\October\\Rain\\Mail\\Mailer) $mailerInstance, (\\Illuminate\\Mail\\Message) $message, (string) $view, (string) $plain, (string) $raw, (array) $data) {\n         *         return false;\n         *     });\n         *\n         * Or\n         *\n         *     $mailerInstance->bindEvent('mailer.beforeAddContent', function ((\\Illuminate\\Mail\\Message) $message, (string) $view, (string) $plain, (string) $raw, (array) $data) {\n         *         return false;\n         *     });\n         *\n         */\n        if (\n            ($this->fireEvent('mailer.beforeAddContent', [$message, $view, $data, $raw, $plain], true) === false) ||\n            (Event::fire('mailer.beforeAddContent', [$this, $message, $view, $data, $raw, $plain], true) === false)\n        ) {\n            return;\n        }\n\n        $html = null;\n        $text = null;\n\n        if (isset($view)) {\n            $viewContent = $this->renderView($view, $data);\n            $result = MailParser::parse($viewContent);\n            $html = $result['html'];\n\n            if ($result['text']) {\n                $text = $result['text'];\n            }\n\n            // Subject\n            $customSubject = $message->getSymfonyMessage()->getSubject();\n            if (\n                empty($customSubject) &&\n                ($subject = Arr::get($result['settings'], 'subject'))\n            ) {\n                $message->subject($subject);\n            }\n        }\n\n        if (isset($plain)) {\n            $text = $this->renderView($plain, $data);\n        }\n\n        if (isset($raw)) {\n            $text = $raw;\n        }\n\n        $this->addContentRaw($message, $html, $text);\n\n        /**\n         * @event mailer.addContent\n         * Fires after the mailer has added content to the message\n         *\n         * Example usage (Logs that content has been added):\n         *\n         *     Event::listen('mailer.addContent', function ((\\October\\Rain\\Mail\\Mailer) $mailerInstance, (\\Illuminate\\Mail\\Message) $message, (string) $view, (array) $data) {\n         *         \\Log::info(\"$view has had content added to the message\");\n         *     });\n         *\n         * Or\n         *\n         *     $mailerInstance->bindEvent('mailer.addContent', function ((\\Illuminate\\Mail\\Message) $message, (string) $view, (array) $data) {\n         *         \\Log::info(\"$view has had content added to the message\");\n         *     });\n         *\n         */\n        $this->fireEvent('mailer.addContent', [$message, $view, $data]);\n        Event::fire('mailer.addContent', [$this, $message, $view, $data]);\n    }\n\n    /**\n     * addContentRaw to a given message.\n     *\n     * @param  \\Illuminate\\Mail\\Message  $message\n     * @param  string  $html\n     * @param  string  $text\n     * @return void\n     */\n    protected function addContentRaw($message, $html, $text)\n    {\n        if (isset($html)) {\n            $message->html($html);\n        }\n\n        if (isset($text)) {\n            $message->text($text);\n        }\n    }\n\n    /**\n     * pretend tells the mailer to not really send messages.\n     *\n     * @param  bool  $value\n     * @return void\n     */\n    public function pretend($value = true)\n    {\n        if ($value) {\n            $this->pretendingOriginal = Config::get('mail.driver');\n\n            Config::set('mail.driver', 'log');\n        }\n        else {\n            Config::set('mail.driver', $this->pretendingOriginal);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Network/Http.php",
    "content": "<?php namespace October\\Rain\\Network;\n\nuse Illuminate\\Support\\Arr;\nuse October\\Rain\\Exception\\ApplicationException;\n\n/**\n * Http Network Access is used as a cURL wrapper for the HTTP protocol\n *\n * @package october\\network\n * @author Alexey Bobkov, Samuel Georges\n *\n * Usage:\n *\n *   Http::get('http://octobercms.com');\n *   Http::post('...');\n *   Http::delete('...');\n *   Http::patch('...');\n *   Http::put('...');\n *   Http::options('...');\n *\n *   $result = Http::post('http://octobercms.com');\n *   echo $result;                          // Outputs: <html><head><title>...\n *   echo $result->code;                    // Outputs: 200\n *   echo $result->headers['Content-Type']; // Outputs: text/html; charset=UTF-8\n *\n *   Http::post('http://octobercms.com', function($http){\n *\n *       // Sets a HTTP header\n *       $http->header('Rest-Key', '...');\n *\n *       // Set a proxy of type (http, socks4, socks5)\n *       $http->proxy('type', 'host', 'port', 'username', 'password');\n *\n *       // Use basic authentication\n *       $http->auth('user', 'pass');\n *\n *       // Sends data with the request\n *       $http->data('foo', 'bar');\n *       $http->data(['key' => 'value', ...]);\n *\n *       // Disable redirects\n *       $http->noRedirect();\n *\n *       // Check host SSL certificate\n *       $http->verifySSL();\n *\n *       // Sets the timeout duration\n *       $http->timeout(3600);\n *\n *       // Write response to a file\n *       $http->toFile('some/path/to/a/file.txt');\n *\n *       // Sets a cURL option manually\n *       $http->setOption(CURLOPT_SSL_VERIFYHOST, false);\n *\n *   });\n *\n */\nclass Http\n{\n    const METHOD_GET = 'GET';\n    const METHOD_POST = 'POST';\n    const METHOD_DELETE = 'DELETE';\n    const METHOD_PATCH = 'PATCH';\n    const METHOD_PUT = 'PUT';\n    const METHOD_OPTIONS = 'OPTIONS';\n\n    /**\n     * @var string url is the HTTP address to use\n     */\n    public $url;\n\n    /**\n     * @var string method the request should use\n     */\n    public $method;\n\n    /**\n     * @var array headers to be sent with the request\n     */\n    public $headers = [];\n\n    /**\n     * @var callable headerCallbackFunc is a custom function for handling response headers\n     */\n    public $headerCallbackFunc;\n\n    /**\n     * @var string body is the last response body\n     */\n    public $body = '';\n\n    /**\n     * @var string rawBody is the last response body (without headers extracted)\n     */\n    public $rawBody = '';\n\n    /**\n     * @var array code is the last returned HTTP code\n     */\n    public $code;\n\n    /**\n     * @var array info is the cURL response information\n     */\n    public $info;\n\n    /**\n     * @var array requestOptions contains cURL Options\n     */\n    public $requestOptions;\n\n    /**\n     * @var array requestData\n     */\n    public $requestData;\n\n    /**\n     * @var array requestHeaders\n     */\n    public $requestHeaders;\n\n    /**\n     * @var string argumentSeparator\n     */\n    public $argumentSeparator = '&';\n\n    /**\n     * @var string streamFile is the file to use when writing to a file\n     */\n    public $streamFile;\n\n    /**\n     * @var string streamFilter is the filter to apply when writing response to a file\n     */\n    public $streamFilter;\n\n    /**\n     * @var int maxRedirects allowed\n     */\n    public $maxRedirects = 10;\n\n    /**\n     * @var int redirectCount is an internal counter\n     */\n    protected $redirectCount = null;\n\n    /**\n     * @var bool hasFileData determines if files are being sent with the request\n     */\n    protected $hasFileData = false;\n\n    /**\n     * make the object with common properties\n     * @param string   $url     HTTP request address\n     * @param string   $method  Request method (GET, POST, PUT, DELETE, etc)\n     * @param callable $options Callable helper function to modify the object\n     */\n    public static function make($url, $method, $options = null): Http\n    {\n        $http = new self;\n        $http->url = $url;\n        $http->method = $method;\n\n        if ($options && is_callable($options)) {\n            $options($http);\n        }\n\n        return $http;\n    }\n\n    /**\n     * get makes a HTTP GET call\n     * @param string $url\n     * @param array  $options\n     * @return self\n     */\n    public static function get($url, $options = null): Http\n    {\n        $http = self::make($url, self::METHOD_GET, $options);\n        return $http->send();\n    }\n\n    /**\n     * post makes a HTTP POST call\n     * @param string $url\n     * @param array  $options\n     * @return self\n     */\n    public static function post($url, $options = null): Http\n    {\n        $http = self::make($url, self::METHOD_POST, $options);\n        return $http->send();\n    }\n\n    /**\n     * delete makes a HTTP DELETE call\n     * @param string $url\n     * @param array  $options\n     * @return self\n     */\n    public static function delete($url, $options = null): Http\n    {\n        $http = self::make($url, self::METHOD_DELETE, $options);\n        return $http->send();\n    }\n\n    /**\n     * patch makes a HTTP PATCH call\n     * @param string $url\n     * @param array  $options\n     * @return self\n     */\n    public static function patch($url, $options = null): Http\n    {\n        $http = self::make($url, self::METHOD_PATCH, $options);\n        return $http->send();\n    }\n\n    /**\n     * put makes a HTTP PUT call\n     * @param string $url\n     * @param array  $options\n     */\n    public static function put($url, $options = null): Http\n    {\n        $http = self::make($url, self::METHOD_PUT, $options);\n        return $http->send();\n    }\n\n    /**\n     * options makes a HTTP OPTIONS call\n     * @param string $url\n     * @param array  $options\n     */\n    public static function options($url, $options = null): Http\n    {\n        $http = self::make($url, self::METHOD_OPTIONS, $options);\n        return $http->send();\n    }\n\n    /**\n     * send the HTTP request\n     */\n    public function send(): Http\n    {\n        if (!function_exists('curl_init')) {\n            echo 'cURL PHP extension required.'.PHP_EOL;\n            exit(1);\n        }\n\n        /*\n         * Create and execute the cURL Resource\n         */\n        $curl = curl_init();\n        curl_setopt($curl, CURLOPT_URL, $this->url);\n        curl_setopt($curl, CURLOPT_HEADER, true);\n        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);\n        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);\n        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);\n\n        if (defined('CURLOPT_FOLLOWLOCATION') && !ini_get('open_basedir')) {\n            curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true);\n            curl_setopt($curl, CURLOPT_MAXREDIRS, $this->maxRedirects);\n        }\n\n        if ($this->requestOptions && is_array($this->requestOptions)) {\n            curl_setopt_array($curl, $this->requestOptions);\n        }\n\n        /*\n         * Set request method\n         */\n        if ($this->method === self::METHOD_POST) {\n            curl_setopt($curl, CURLOPT_POST, true);\n        }\n        elseif ($this->method !== self::METHOD_GET) {\n            curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->method);\n        }\n\n        /*\n         * Set request data\n         */\n        if ($this->requestData) {\n            if (in_array($this->method, [self::METHOD_POST, self::METHOD_PATCH, self::METHOD_PUT])) {\n                curl_setopt($curl, CURLOPT_POSTFIELDS, $this->getRequestData());\n            }\n            elseif (in_array($this->method, [self::METHOD_GET, self::METHOD_DELETE])) {\n                curl_setopt($curl, CURLOPT_URL, $this->url . '?' . $this->getRequestData());\n            }\n        }\n\n        /*\n         * Set request headers\n         */\n        if ($this->requestHeaders) {\n            $requestHeaders = [];\n            foreach ($this->requestHeaders as $key => $value) {\n                $requestHeaders[] = $key . ': ' . $value;\n            }\n\n            curl_setopt($curl, CURLOPT_HTTPHEADER, $requestHeaders);\n        }\n\n        /*\n         * Custom header function\n         */\n        if ($this->headerCallbackFunc) {\n            curl_setopt($curl, CURLOPT_HEADER, false);\n            curl_setopt($curl, CURLOPT_HEADERFUNCTION, $this->headerCallbackFunc);\n        }\n\n        /*\n         * Execute output to file\n         */\n        if ($this->streamFile) {\n            $stream = fopen($this->streamFile, 'w');\n            if ($this->streamFilter) {\n                stream_filter_append($stream, $this->streamFilter, STREAM_FILTER_WRITE);\n            }\n            curl_setopt($curl, CURLOPT_HEADER, false);\n            curl_setopt($curl, CURLOPT_FILE, $stream);\n            curl_exec($curl);\n        }\n        /*\n         * Execute output to variable\n         */\n        else {\n            $response = $this->rawBody = curl_exec($curl);\n            $headerSize = curl_getinfo($curl, CURLINFO_HEADER_SIZE);\n            $this->headers = $this->headerToArray(substr($response, 0, $headerSize));\n            $this->body = substr($response, $headerSize);\n        }\n\n        $this->info = curl_getinfo($curl);\n        $this->code = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);\n\n        /*\n         * Close resources\n         */\n        curl_close($curl);\n\n        if ($this->streamFile) {\n            fclose($stream);\n        }\n\n        /*\n         * Emulate FOLLOW LOCATION behavior\n         */\n        if (!defined('CURLOPT_FOLLOWLOCATION') || ini_get('open_basedir')) {\n            if ($this->redirectCount === null) {\n                $this->redirectCount = $this->maxRedirects;\n            }\n            if (in_array($this->code, [301, 302])) {\n                $this->url = Arr::get($this->info, 'redirect_url');\n                if (!empty($this->url) && $this->redirectCount > 0) {\n                    $this->redirectCount -= 1;\n                    return $this->send();\n                }\n            }\n        }\n\n        return $this;\n    }\n\n    /**\n     * getRequestData returns the request data set\n     */\n    public function getRequestData()\n    {\n        if (empty($this->requestData)) {\n            return isset($this->requestOptions[CURLOPT_POSTFIELDS])\n                ? $this->requestOptions[CURLOPT_POSTFIELDS]\n                : '';\n        }\n\n        if ($this->method === self::METHOD_GET || !$this->hasFileData) {\n            return http_build_query($this->requestData, '', $this->argumentSeparator);\n        }\n\n        // This will trigger multipart/form-data content type and needs an array,\n        // make some attempt at supporting multidimensional array values\n        if (is_array($this->requestData)) {\n            $out = [];\n            foreach ($this->requestData as $var => $dat) {\n                $out[$var] = is_array($dat)\n                    ? http_build_query($dat, '', $this->argumentSeparator)\n                    : $dat;\n            }\n            return $out;\n        }\n\n        return $this->requestData;\n    }\n\n    /**\n     * data added to the request\n     */\n    public function data($key, $value = null): Http\n    {\n        if (is_array($key)) {\n            foreach ($key as $_key => $_value) {\n                $this->data($_key, $_value);\n            }\n            return $this;\n        }\n\n        $this->requestData[$key] = $value;\n\n        return $this;\n    }\n\n    /**\n     * dataFile added to the request\n     */\n    public function dataFile(string $key, string $filePath): Http\n    {\n        $this->hasFileData = true;\n\n        return $this->data($key, curl_file_create($filePath));\n    }\n\n    /**\n     * header added to the request\n     * @param string $value\n     */\n    public function header($key, $value = null): Http\n    {\n        if (is_array($key)) {\n            foreach ($key as $_key => $_value) {\n                $this->header($_key, $_value);\n            }\n            return $this;\n        }\n\n        $this->requestHeaders[$key] = $value;\n\n        return $this;\n    }\n\n    /**\n     * proxy to use with this request\n     */\n    public function proxy($type, $host, $port, $username = null, $password = null): Http\n    {\n        if ($type === 'http') {\n            $this->setOption(CURLOPT_PROXYTYPE, CURLPROXY_HTTP);\n        }\n        elseif ($type === 'socks4') {\n            $this->setOption(CURLOPT_PROXYTYPE, CURLPROXY_SOCKS4);\n        }\n        elseif ($type === 'socks5') {\n            $this->setOption(CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);\n        }\n\n        $this->setOption(CURLOPT_PROXY, $host . ':' . $port);\n\n        if ($username && $password) {\n            $this->setOption(CURLOPT_PROXYUSERPWD, $username . ':' . $password);\n        }\n\n        return $this;\n    }\n\n    /**\n     * auth adds authentication to the request\n     * @param string $user\n     * @param string $pass\n     */\n    public function auth($user, $pass = null): Http\n    {\n        if (strpos($user, ':') !== false && !$pass) {\n            list($user, $pass) = explode(':', $user);\n        }\n\n        $this->setOption(CURLOPT_HTTPAUTH, CURLAUTH_BASIC);\n        $this->setOption(CURLOPT_USERPWD, $user . ':' . $pass);\n\n        return $this;\n    }\n\n    /**\n     * noRedirect disables follow location (redirects)\n     */\n    public function noRedirect(): Http\n    {\n        if (defined('CURLOPT_FOLLOWLOCATION') && !ini_get('open_basedir')) {\n            $this->setOption(CURLOPT_FOLLOWLOCATION, false);\n        }\n        else {\n            $this->maxRedirects = 0;\n        }\n\n        return $this;\n    }\n\n    /**\n     * verifySSL enabled for the request\n     */\n    public function verifySSL(): Http\n    {\n        $this->setOption(CURLOPT_SSL_VERIFYPEER, true);\n        $this->setOption(CURLOPT_SSL_VERIFYHOST, true);\n        return $this;\n    }\n\n    /**\n     * timeout for the request\n     * @param string $timeout\n     */\n    public function timeout($timeout): Http\n    {\n        $this->setOption(CURLOPT_CONNECTTIMEOUT, $timeout);\n        $this->setOption(CURLOPT_TIMEOUT, $timeout);\n        return $this;\n    }\n\n    /**\n     * toFile write the response to a file\n     * @param  string $path   Path to file\n     * @param  string $filter Stream filter as listed in stream_get_filters()\n     */\n    public function toFile($path, $filter = null): Http\n    {\n        $this->streamFile = $path;\n\n        if ($filter) {\n            $this->streamFilter = $filter;\n        }\n\n        return $this;\n    }\n\n    /**\n     * headerCallback sets a custom method for handling headers\n     *\n     *     function header_callback($curl, string $headerLine) {}\n     *\n     */\n    public function headerCallback($callback): Http\n    {\n        $this->headerCallbackFunc = $callback;\n\n        return $this;\n    }\n\n    /**\n     * setOption as a single option to the request\n     * @param string $option\n     * @param string $value\n     */\n    public function setOption($option, $value = null): Http\n    {\n        if (is_array($option)) {\n            foreach ($option as $_option => $_value) {\n                $this->setOption($_option, $_value);\n            }\n            return $this;\n        }\n\n        if (is_string($option) && defined($option)) {\n            $optionKey = constant($option);\n            $this->requestOptions[$optionKey] = $value;\n        }\n        elseif (is_int($option)) {\n            $constants = get_defined_constants(true);\n            $curlOptConstants = array_flip(array_filter($constants['curl'], function ($key) {\n                return strpos($key, 'CURLOPT_') === 0;\n            }, ARRAY_FILTER_USE_KEY));\n\n            if (isset($curlOptConstants[$option])) {\n                $this->requestOptions[$option] = $value;\n            }\n            else {\n                throw new ApplicationException('$option parameter must be a CURLOPT constant or equivalent integer');\n            }\n        }\n        else {\n            throw new ApplicationException('$option parameter must be a CURLOPT constant or equivalent integer');\n        }\n\n        return $this;\n    }\n\n    /**\n     * Handy if this object is called directly.\n     * @return string The last response.\n     */\n    public function __toString()\n    {\n        return (string) $this->body;\n    }\n\n    /**\n     * headerToArray turns a header string into an array\n     */\n    protected function headerToArray(string $header): array\n    {\n        $headers = [];\n        $parts = explode(\"\\r\\n\", $header);\n\n        foreach ($parts as $singleHeader) {\n            $delimiter = strpos($singleHeader, ': ');\n            if ($delimiter !== false) {\n                $key = substr($singleHeader, 0, $delimiter);\n                $val = substr($singleHeader, $delimiter + 2);\n                $headers[$key] = $val;\n            }\n            else {\n                $delimiter = strpos($singleHeader, ' ');\n                if ($delimiter !== false) {\n                    $key = substr($singleHeader, 0, $delimiter);\n                    $val = substr($singleHeader, $delimiter + 1);\n                    $headers[$key] = $val;\n                }\n            }\n        }\n\n        return $headers;\n    }\n}\n"
  },
  {
    "path": "src/Parse/Bracket.php",
    "content": "<?php namespace October\\Rain\\Parse;\n\n/**\n * Bracket parser\n *\n * @package october\\parse\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Bracket\n{\n    const CHAR_OPEN = '{';\n    const CHAR_CLOSE = '}';\n\n    /**\n     * @var array Parsing options\n     */\n    protected $options = [\n        'encodeHtml' => false,\n        'newlineToBr' => false,\n        'filters' => []\n    ];\n\n    public function __construct($options = [])\n    {\n        $this->setOptions($options);\n    }\n\n    public function setOptions($options = [])\n    {\n        $this->options = array_merge($this->options, $options);\n    }\n\n    /**\n     * Static helper for new instances of this class.\n     * @param  string $template\n     * @param  array $vars\n     * @param  array $options\n     * @return self\n     */\n    public static function parse($template, $vars = [], $options = [])\n    {\n        $obj = new static($options);\n        return $obj->parseString($template, $vars);\n    }\n\n    /**\n     * Parse a string against data\n     * @param  string $string\n     * @param  array $data\n     * @return string\n     */\n    public function parseString($string, $data)\n    {\n        if (!is_string($string) || !strlen(trim($string))) {\n            return false;\n        }\n\n        foreach ($data as $key => $value) {\n            if (is_array($value)) {\n                $string = $this->parseLoop($key, $value, $string);\n            }\n            else {\n                $string = $this->parseKey($key, $value, $string);\n                $string = $this->parseKeyFilters($key, $value, $string);\n                $string = $this->parseKeyBooleans($key, $value, $string);\n            }\n        }\n\n        return $string;\n    }\n\n    /**\n     * Process a single key\n     * @param  string $key\n     * @param  string $value\n     * @param  string $string\n     * @return string\n     */\n    protected function parseKey($key, $value, $string)\n    {\n        if (isset($this->options['encodeHtml']) && $this->options['encodeHtml']) {\n            $value = htmlentities($value, ENT_QUOTES, 'UTF-8', false);\n        }\n\n        if (isset($this->options['newlineToBr']) && $this->options['newlineToBr']) {\n            $value = nl2br($value);\n        }\n\n        $returnStr = str_replace(static::CHAR_OPEN.$key.static::CHAR_CLOSE, $value, $string);\n\n        return $returnStr;\n    }\n\n    /**\n     * Look for filtered variables and replace them\n     * @param  string $key\n     * @param  string $value\n     * @param  string $string\n     * @return string\n     */\n    protected function parseKeyFilters($key, $value, $string)\n    {\n        if (!$filters = $this->options['filters']) {\n            return $string;\n        }\n\n        $returnStr = $string;\n\n        foreach ($filters as $filter => $func) {\n            $charKey = static::CHAR_OPEN.$key.'|'.$filter.static::CHAR_CLOSE;\n\n            if (is_callable($func) && strpos($string, $charKey) !== false) {\n                $returnStr = str_replace($charKey, $func($value), $returnStr);\n            }\n        }\n\n        return $returnStr;\n    }\n\n    /**\n     * This is an internally used method, the syntax is experimental and may change.\n     */\n    protected function parseKeyBooleans($key, $value, $string)\n    {\n        $openKey = static::CHAR_OPEN.'?'.$key.static::CHAR_CLOSE;\n        $closeKey = static::CHAR_OPEN.'/'.$key.static::CHAR_CLOSE;\n\n        if ($value) {\n            $returnStr = str_replace([$openKey, $closeKey], '', $string);\n        }\n        else {\n            $open = preg_quote($openKey);\n            $close = preg_quote($closeKey);\n            $returnStr = preg_replace('|'.$open.'[\\s\\S]+?'.$close.'|s', '', $string);\n        }\n\n        return $returnStr;\n    }\n\n    /**\n     * Search for open/close keys and process them in a nested fashion\n     * @param  string $key\n     * @param  array  $data\n     * @param  string $string\n     * @return string\n     */\n    protected function parseLoop($key, $data, $string)\n    {\n        $returnStr = '';\n        $match = $this->parseLoopRegex($string, $key);\n\n        if (!$match) {\n            return $string;\n        }\n\n        foreach ($data as $row) {\n            $matchedText = $match[1];\n\n            foreach ($row as $key => $value) {\n                if (is_array($value)) {\n                    $matchedText = $this->parseLoop($key, $value, $matchedText);\n                }\n                else {\n                    $matchedText = $this->parseKey($key, $value, $matchedText);\n                    $matchedText = $this->parseKeyFilters($key, $value, $matchedText);\n                    $matchedText = $this->parseKeyBooleans($key, $value, $matchedText);\n                }\n            }\n\n            $returnStr .= $matchedText;\n        }\n\n        return str_replace($match[0], $returnStr, $string);\n    }\n\n    /**\n     * Internal method, returns a Regular expression for parsing\n     * a looping tag.\n     * @param  string $string\n     * @param  string $key\n     * @return string\n     */\n    protected function parseLoopRegex($string, $key)\n    {\n        $open = preg_quote(static::CHAR_OPEN);\n        $close = preg_quote(static::CHAR_CLOSE);\n\n        $regex = '|';\n        $regex .= $open.$key.$close; // Open\n        $regex .= '(.+?)'; // Content\n        $regex .= $open.'/'.$key.$close; // Close\n        $regex .='|s';\n\n        preg_match($regex, $string, $match);\n        return $match ?: false;\n    }\n}\n"
  },
  {
    "path": "src/Parse/ComponentParser.php",
    "content": "<?php namespace October\\Rain\\Parse;\n\n/**\n * ComponentParser helper class\n *\n * @package october\\parse\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ComponentParser\n{\n    /**\n     * parse contents\n     */\n    public function parse($contents)\n    {\n        return $this->parseContentInternal($contents);\n    }\n\n    /**\n     * parseContentInternal handler\n     */\n    protected function parseContentInternal($content)\n    {\n        // Optimized regular expression to match self-closing and nested custom HTML elements\n        $pattern = '/<x-([a-zA-Z0-9-]+)(\\s+[^>]*)?\\/>|<x-([a-zA-Z0-9-]+)(\\s+[^>]*)?>(.*?)<\\/x-\\3>/s';\n\n        // Find all matches\n        preg_match_all($pattern, $content, $matches, PREG_SET_ORDER);\n\n        foreach ($matches as $match) {\n            // Self-closing tag\n            if (!empty($match[1])) {\n                $tag = $match[1];\n                $attributes = $this->parseAttributes($match[2]);\n                $replacement = $this->parseContent($tag, $attributes);\n                $content = str_replace($match[0], $replacement, $content);\n            }\n            // Nested tag\n            elseif (!empty($match[3])) {\n                $tag = $match[3];\n                $attributes = $this->parseAttributes($match[4]);\n                $attributes['slot'] = $this->parseContentInternal(trim($match[5]));\n                $replacement = $this->parseContent($tag, $attributes);\n                $content = str_replace($match[0], $replacement, $content);\n            }\n        }\n\n        return $content;\n    }\n\n    /**\n     * parseAttributes extracts attributes from the component tag\n     */\n    protected function parseAttributes($attributeString)\n    {\n        $attributes = [];\n        if (preg_match_all('/(\\w+)=\"([^\"]*)\"/', $attributeString, $attrMatches, PREG_SET_ORDER)) {\n            foreach ($attrMatches as $attr) {\n                $attributes[$attr[1]] = $attr[2];\n            }\n        }\n        return $attributes;\n    }\n\n    /**\n     * parseContent uses a global registry of components\n     */\n    protected function parseContent($tag, $attributes)\n    {\n        // ...@todo...\n        return \"<div data-component=\\\"{$tag}\\\"></div>\";\n    }\n}\n"
  },
  {
    "path": "src/Parse/Ini.php",
    "content": "<?php namespace October\\Rain\\Parse;\n\nuse Exception;\n\n/**\n * Ini (Initialization) configuration parser that uses \"October-flavoured INI\",\n * with parsing that supports infinite array nesting and the ability to render\n * INI syntax from a PHP array.\n *\n * @package october\\parse\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Ini\n{\n    /**\n     * parse supplied INI contents in to a PHP array.\n     * @param string $contents INI contents to parse.\n     * @return array\n     */\n    public function parse($contents)\n    {\n        $contents = $this->parsePreProcess($contents);\n        $contents = parse_ini_string($contents, true);\n        $contents = $this->parsePostProcess($contents);\n        return $contents;\n    }\n\n    /**\n     * parseFile supplied INI file contents in to a PHP array.\n     * @param string $fileName File to read contents and parse.\n     * @return array\n     */\n    public function parseFile($fileName)\n    {\n        $contents = file_get_contents($fileName);\n        return $this->parse($contents);\n    }\n\n    /**\n     * render formats an INI formatted string from an array of data variables\n     *\n     * Supported options:\n     * - exceptionOnInvalidKey: if an exception must be thrown on invalid key names.\n     *\n     * @param array $vars\n     * @param array $options\n     * @return string\n     */\n    public function render($vars = [], $options = [])\n    {\n        extract(array_merge([\n            'exceptionOnInvalidKey' => false,\n        ], $options));\n\n        $content = '';\n        $sections = [];\n\n        foreach ($vars as $key => $value) {\n            if ($this->validateKeyName($key) !== true) {\n                if ($exceptionOnInvalidKey) {\n                    throw new Exception(\"Key name [$key] is invalid for INI syntax\");\n                }\n                continue;\n            }\n\n            if (is_array($value)) {\n                if ($this->isFinalArray($value)) {\n                    foreach ($value as $_value) {\n                        $content .= $key.'[] = '.$this->evalValue($_value).PHP_EOL;\n                    }\n                }\n                else {\n                    $sections[$key] = $this->renderProperties($value);\n                }\n            }\n            elseif (strlen($value)) {\n                $content .= $key.' = '.$this->evalValue($value).PHP_EOL;\n            }\n        }\n\n        foreach ($sections as $key => $section) {\n            $content .= PHP_EOL.'['.$key.']'.PHP_EOL.$section;\n        }\n\n        return trim($content);\n    }\n\n    //\n    // Parse\n    //\n\n    /**\n     * parsePreProcess converts key names traditionally invalid, \"][\", and\n     * replaces them with a valid character \"|\" so parse_ini_string\n     * can function correctly. It also forces arrays to have unique\n     * indexes so their integrity is maintained.\n     * @param string $contents INI contents to parse.\n     * @return string\n     */\n    protected function parsePreProcess($contents)\n    {\n        // Sanitize environment variable interpolation syntax to prevent\n        // parse_ini_string from resolving ${VAR} to environment values\n        $contents = str_replace('${', 'oc_env_open{', $contents);\n\n        // Normalize EOL\n        $contents = preg_replace('~\\R~u', PHP_EOL, $contents);\n        $contents = explode(PHP_EOL, $contents);\n        $count = 0;\n        $lastName = null;\n\n        foreach ($contents as $key => $content) {\n            if (strpos($content, '=') === false) {\n                continue;\n            }\n\n            $parts = explode('=', $content, 2);\n            if (count($parts) < 2) {\n                continue;\n            }\n\n            $varName = $parts[0];\n            if ($lastName !== $varName) {\n                $count = 0;\n                $lastName = null;\n            }\n\n            if (\n                ($lastName === null || $lastName === $varName) &&\n                strpos($varName, '[]') !== false\n            ) {\n                $varName = str_replace('[]', '['.$count.']', $varName);\n                $count++;\n            }\n\n            $lastName = $parts[0];\n            $parts[0] = str_replace('][', '|', $varName);\n            $contents[$key] = implode('=', $parts);\n        }\n\n        return implode(PHP_EOL, $contents);\n    }\n\n    /**\n     * parsePostProcess takes the valid key name from pre processing and\n     * converts it back to a real PHP array. Eg:\n     * - name[validation|regex|message]\n     * Converts to:\n     * - name => [validation => [regex => [message]]]\n     * @param array $array\n     * @return array\n     */\n    protected function parsePostProcess($array)\n    {\n        $result = [];\n\n        foreach ($array as $key => &$value) {\n            if (is_string($value)) {\n                $value = str_replace('oc_env_open{', '${', $value);\n            }\n\n            $this->expandProperty($result, $key, $value);\n\n            if (is_array($value)) {\n                $result[$key] = $this->parsePostProcess($value);\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * expandProperty expands a single array property from traditional INI syntax.\n     * If no key is given to the method, the entire array will be replaced.\n     * @param  array   $array\n     * @param  string  $key\n     * @param  mixed   $value\n     * @return array\n     */\n    protected function expandProperty(&$array, $key, $value)\n    {\n        if (is_null($key)) {\n            return $array = $value;\n        }\n\n        $keys = explode('|', $key);\n\n        while (count($keys) > 1) {\n            $key = array_shift($keys);\n\n            if (!isset($array[$key]) || !is_array($array[$key])) {\n                $array[$key] = [];\n            }\n\n            $array =& $array[$key];\n        }\n\n        $array[array_shift($keys)] = $value;\n\n        return $array;\n    }\n\n    //\n    // Render\n    //\n\n    /**\n     * renderProperties renders section properties.\n     * @param array $vars\n     * @return string\n     */\n    protected function renderProperties($vars = [])\n    {\n        $content = '';\n\n        foreach ($vars as $key => $value) {\n            if (is_array($value)) {\n                if ($this->isFinalArray($value)) {\n                    foreach ($value as $_value) {\n                        $content .= $key.'[] = '.$this->evalValue($_value).PHP_EOL;\n                    }\n                }\n                else {\n                    $value = $this->flattenProperties($value);\n                    foreach ($value as $_key => $_value) {\n                        if (is_array($_value)) {\n                            foreach ($_value as $__value) {\n                                $content .= $key.'['.$_key.'][] = '.$this->evalValue($__value).PHP_EOL;\n                            }\n                        }\n                        else {\n                            $content .= $key.'['.$_key.'] = '.$this->evalValue($_value).PHP_EOL;\n                        }\n                    }\n                }\n            }\n            elseif (strlen($value)) {\n                $content .= $key.' = '.$this->evalValue($value).PHP_EOL;\n            }\n        }\n\n        return $content;\n    }\n\n    /**\n     * flattenProperties flattens a multi-dimensional associative array for traditional INI syntax.\n     * @param  array   $array\n     * @param  string  $prepend\n     * @return array\n     */\n    protected function flattenProperties($array, $prepend = '')\n    {\n        $results = [];\n\n        foreach ($array as $key => $value) {\n            if (is_array($value)) {\n                if ($this->isFinalArray($value)) {\n                    $results[$prepend.$key] = $value;\n                }\n                else {\n                    $results = array_merge($results, $this->flattenProperties($value, $prepend.$key.']['));\n                }\n            }\n            else {\n                $results[$prepend.$key] = $value;\n            }\n        }\n\n        return $results;\n    }\n\n    /**\n     * evalValue converts a PHP value to make it suitable for INI format.\n     * Strings are escaped.\n     * @param string $value Specifies the value to process\n     * @return string Returns the processed value\n     */\n    protected function evalValue($value)\n    {\n        // Numeric\n        if (is_numeric($value)) {\n            return $value;\n        }\n\n        // String (default)\n        $value = str_replace('\"', '\\\"', $value);\n        $value = preg_replace('~\\\\\\\"([\\r\\n])~', '\\\\\\\"\"\"$1', $value);\n\n        return '\"'.$value.'\"';\n    }\n\n    /**\n     * isFinalArray checks if the array is the final node in a multidimensional array.\n     * Checked supplied array is not associative and contains no array values.\n     * @param array $array\n     * @return bool\n     */\n    protected function isFinalArray(array $array)\n    {\n        return !empty($array) &&\n            !count(array_filter($array, 'is_array')) &&\n            !count(array_filter(array_keys($array), 'is_string'));\n    }\n\n    /**\n     * validateKeyName returns false if an invalid key name is found\n     */\n    protected function validateKeyName($keyName): bool\n    {\n        $invalidChars = '?{}|&~!()^\"#;=';\n        foreach (str_split($invalidChars) as $char) {\n            if (strpos($keyName, $char) !== false) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/Parse/Markdown.php",
    "content": "<?php namespace October\\Rain\\Parse;\n\nuse Html;\nuse Event;\nuse October\\Rain\\Parse\\Parsedown\\ParsedownExtra;\n\n/**\n * Markdown helper class.\n *\n * Calling Markdown::parse($text) returns the HTML corresponding\n * to the Markdown input in $text.\n *\n * October CMS uses ParsedownExtra as its Markdown parser,\n * but fires markdown.beforeParse and markdown.parse events\n * allowing hooks into the default parsing,\n *\n * The markdown.beforeParse event passes a MarkdownData\n * instance, containing a public $text variable. Event\n * listeners can modify $text, for example to filter out\n * or protect snippets from being interpreted by ParseDown.\n *\n * Similarly, markdown.parse is fired after ParseDown has\n * interpreted the (possibly modified) input. This event\n * passes an array [$text, $data], where $text is the\n * original unmodified Markdown input, and $data is the HTML\n * code generated by ParseDown.\n *\n * @package october\\parse\n * @author Alexey Bobkov, Samuel Georges\n **/\nclass Markdown\n{\n    use \\October\\Rain\\Support\\Traits\\Emitter;\n\n    /**\n     * @var ParsedownExtra parser is the parsedown instance\n     */\n    protected $parser;\n\n    /**\n     * parse text using Markdown and Markdown-Extra\n     */\n    public function parse($text): string\n    {\n        return $this->parseInternal($text);\n    }\n\n    /**\n     * parseClean enables safe mode where the resulting HTML is cleaned\n     * using a sanitizer\n     */\n    public function parseClean($text): string\n    {\n        $result = Html::clean($this->parse($text));\n\n        $this->parser = null;\n\n        return $result;\n    }\n\n    /**\n     * parseSafe is stricter than parse clean allowing no HTML at all\n     * except for basic protocols such as https://, ftps://, mailto:, etc.\n     */\n    public function parseSafe($text): string\n    {\n        $this->getParser()->setSafeMode(true);\n\n        $result = $this->parse($text);\n\n        $this->parser = null;\n\n        return $result;\n    }\n\n    /**\n     * parseIndent disables code blocks caused by indentation\n     */\n    public function parseIndent($text): string\n    {\n        $this->getParser()->setIndentMode(false);\n\n        $result = $this->parse($text);\n\n        $this->parser = null;\n\n        return $result;\n    }\n\n    /**\n     * parseLine parses a single line\n     */\n    public function parseLine($text): string\n    {\n        return $this->parseInternal($text, 'line');\n    }\n\n    /**\n     * parseInternal is an internal method for parsing\n     */\n    protected function parseInternal($text, $method = 'text'): string\n    {\n        $data = new MarkdownData($text);\n\n        $this->fireEvent('beforeParse', $data, false);\n        Event::fire('markdown.beforeParse', $data, false);\n\n        $result = $data->text;\n\n        $result = $this->getParser()->$method($result);\n\n        $data->text = $result;\n\n        // The markdown.parse gets passed both the original\n        // input and the result so far.\n        $this->fireEvent('parse', [$text, $data], false);\n        Event::fire('markdown.parse', [$text, $data], false);\n\n        return $data->text;\n    }\n\n    /**\n     * getParser returns an instance of the parser\n     */\n    protected function getParser(): ParsedownExtra\n    {\n        if ($this->parser === null) {\n            $this->parser = new ParsedownExtra;\n        }\n\n        return $this->parser;\n    }\n}\n"
  },
  {
    "path": "src/Parse/MarkdownData.php",
    "content": "<?php namespace October\\Rain\\Parse;\n\n/**\n * Helper class for passing partially parsed Markdown input\n * to and from the markdown.beforeParse and markdown.parse\n * event handlers\n *\n * @package october\\support\n * @author Alexey Bobkov, Samuel Georges\n */\nclass MarkdownData\n{\n    /**\n     * @var string\n     */\n    public $text;\n\n    public function __construct($text)\n    {\n        $this->text = $text;\n    }\n}\n"
  },
  {
    "path": "src/Parse/ParseServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Parse;\n\nuse Illuminate\\Support\\ServiceProvider;\nuse Illuminate\\Contracts\\Support\\DeferrableProvider;\n\n/**\n * ParseServiceProvider\n */\nclass ParseServiceProvider extends ServiceProvider implements DeferrableProvider\n{\n    /**\n     * register the service provider.\n     */\n    public function register()\n    {\n        $this->app->singleton('parse.markdown', function ($app) {\n            return new Markdown;\n        });\n\n        $this->app->singleton('parse.yaml', function ($app) {\n            return new Yaml;\n        });\n\n        $this->app->singleton('parse.twig', function ($app) {\n            return new Twig;\n        });\n\n        $this->app->singleton('parse.ini', function ($app) {\n            return new Ini;\n        });\n    }\n\n    /**\n     * provides the returned services.\n     * @return array\n     */\n    public function provides()\n    {\n        return [\n            'parse.markdown',\n            'parse.yaml',\n            'parse.twig',\n            'parse.ini'\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Parse/Parsedown/Parsedown.php",
    "content": "<?php namespace October\\Rain\\Parse\\Parsedown;\n\n/**\n * Parsedown\n */\nclass Parsedown\n{\n    /**\n     * @var mixed breaksEnabled\n     */\n    protected $breaksEnabled;\n\n    /**\n     * @var mixed markupEscaped\n     */\n    protected $markupEscaped;\n\n    /**\n     * @var mixed urlsLinked\n     */\n    protected $urlsLinked = true;\n\n    /**\n     * @var mixed safeMode\n     */\n    protected $safeMode;\n\n    /**\n     * @var mixed strictMode\n     */\n    protected $strictMode;\n\n    /**\n     * @var array BlockTypes\n     */\n    protected $BlockTypes = [\n        '#' => ['Header'],\n        '*' => ['Rule', 'List'],\n        '+' => ['List'],\n        '-' => ['SetextHeader', 'Table', 'Rule', 'List'],\n        '0' => ['List'],\n        '1' => ['List'],\n        '2' => ['List'],\n        '3' => ['List'],\n        '4' => ['List'],\n        '5' => ['List'],\n        '6' => ['List'],\n        '7' => ['List'],\n        '8' => ['List'],\n        '9' => ['List'],\n        ':' => ['Table'],\n        '<' => ['Comment', 'Markup'],\n        '=' => ['SetextHeader'],\n        '>' => ['Quote'],\n        '[' => ['Reference'],\n        '_' => ['Rule'],\n        '`' => ['FencedCode'],\n        '|' => ['Table'],\n        '~' => ['FencedCode'],\n    ];\n\n    /**\n     * @var array unmarkedBlockTypes\n     */\n    protected $unmarkedBlockTypes = [\n        'Code',\n    ];\n\n    /**\n     * @var mixed safeLinksWhitelist\n     */\n    protected $safeLinksWhitelist = array(\n        'http://',\n        'https://',\n        'ftp://',\n        'ftps://',\n        'mailto:',\n        'tel:',\n        'data:image/png;base64,',\n        'data:image/gif;base64,',\n        'data:image/jpeg;base64,',\n        'irc:',\n        'ircs:',\n        'git:',\n        'ssh:',\n        'news:',\n        'steam:',\n    );\n\n    /**\n     * @var array InlineTypes\n     */\n    protected $InlineTypes = array(\n        '!' => ['Image'],\n        '&' => ['SpecialCharacter'],\n        '*' => ['Emphasis'],\n        ':' => ['Url'],\n        '<' => ['UrlTag', 'EmailTag', 'Markup'],\n        '[' => ['Link'],\n        '_' => ['Emphasis'],\n        '`' => ['Code'],\n        '~' => ['Strikethrough'],\n        '\\\\' => ['EscapeSequence'],\n    );\n\n    /**\n     * @var string inlineMarkerList\n     */\n    protected $inlineMarkerList = '!*_&[:<`~\\\\';\n\n    /**\n     * @var array instances\n     */\n    protected static $instances = [];\n\n    /**\n     * @var array DefinitionData\n     */\n    protected $DefinitionData;\n\n    /**\n     * @var array specialCharacters\n     */\n    protected $specialCharacters = [\n        '\\\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~'\n    ];\n\n    /**\n     * @var array StrongRegex\n     */\n    protected $StrongRegex = [\n        '*' => '/^[*]{2}((?:\\\\\\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s',\n        '_' => '/^__((?:\\\\\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us',\n    ];\n\n    /**\n     * @var array EmRegex\n     */\n    protected $EmRegex = [\n        '*' => '/^[*]((?:\\\\\\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',\n        '_' => '/^_((?:\\\\\\\\_|[^_]|__[^_]*__)+?)_(?!_)\\b/us',\n    ];\n\n    /**\n     * @var array regexHtmlAttribute\n     */\n    protected $regexHtmlAttribute = '[a-zA-Z_:][\\w:.-]*+(?:\\s*+=\\s*+(?:[^\"\\'=<>`\\s]+|\"[^\"]*+\"|\\'[^\\']*+\\'))?+';\n\n    /**\n     * @var array voidElements\n     */\n    protected $voidElements = [\n        'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',\n    ];\n\n    /**\n     * @var array textLevelElements\n     */\n    protected $textLevelElements = [\n        'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',\n        'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',\n        'i', 'rp', 'del', 'code',          'strike', 'marquee',\n        'q', 'rt', 'ins', 'font',          'strong',\n        's', 'tt', 'kbd', 'mark',\n        'u', 'xm', 'sub', 'nobr',\n                   'sup', 'ruby',\n                   'var', 'span',\n                   'wbr', 'time',\n    ];\n\n    /**\n     * text\n     */\n    public function text($text)\n    {\n        $Elements = $this->textElements($text);\n\n        // Convert to markup\n        $markup = $this->elements($Elements);\n\n        // Trim line breaks\n        $markup = trim($markup, \"\\n\");\n\n        return $markup;\n    }\n\n    /**\n     * textElements\n     */\n    protected function textElements($text)\n    {\n        // make sure no definitions are set\n        $this->DefinitionData = [];\n\n        // standardize line breaks\n        $text = str_replace(array(\"\\r\\n\", \"\\r\"), \"\\n\", $text);\n\n        // remove surrounding line breaks\n        $text = trim($text, \"\\n\");\n\n        // split text into lines\n        $lines = explode(\"\\n\", $text);\n\n        // iterate through lines to identify blocks\n        return $this->linesElements($lines);\n    }\n\n    /**\n     * setBreaksEnabled\n     */\n    public function setBreaksEnabled($breaksEnabled)\n    {\n        $this->breaksEnabled = $breaksEnabled;\n\n        return $this;\n    }\n\n    /**\n     * setMarkupEscaped\n     */\n    public function setMarkupEscaped($markupEscaped)\n    {\n        $this->markupEscaped = $markupEscaped;\n\n        return $this;\n    }\n\n    /**\n     * setUrlsLinked\n     */\n    public function setUrlsLinked($urlsLinked)\n    {\n        $this->urlsLinked = $urlsLinked;\n\n        return $this;\n    }\n\n    /**\n     * setSafeMode\n     */\n    public function setSafeMode($safeMode)\n    {\n        $this->safeMode = (bool) $safeMode;\n\n        return $this;\n    }\n\n    /**\n     * setStrictMode\n     */\n    public function setStrictMode($strictMode)\n    {\n        $this->strictMode = (bool) $strictMode;\n\n        return $this;\n    }\n\n    /**\n     * setIndentMode\n     */\n    public function setIndentMode($indentMode)\n    {\n        $this->unmarkedBlockTypes = $indentMode === false ? ['Markup'] : ['Code'];\n\n        return $this;\n    }\n\n    /**\n     * lines\n     */\n    protected function lines(array $lines)\n    {\n        return $this->elements($this->linesElements($lines));\n    }\n\n    /**\n     * linesElements\n     */\n    protected function linesElements(array $lines)\n    {\n        $Elements = [];\n        $CurrentBlock = null;\n\n        foreach ($lines as $line) {\n            if (chop($line) === '') {\n                if (isset($CurrentBlock)) {\n                    $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted'])\n                        ? $CurrentBlock['interrupted'] + 1 : 1\n                    );\n                }\n\n                continue;\n            }\n\n            while (($beforeTab = strstr($line, \"\\t\", true)) !== false) {\n                $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4;\n\n                $line = $beforeTab\n                    . str_repeat(' ', $shortage)\n                    . substr($line, strlen($beforeTab) + 1)\n                ;\n            }\n\n            $indent = strspn($line, ' ');\n\n            $text = $indent > 0 ? substr($line, $indent) : $line;\n\n            $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);\n\n            if (isset($CurrentBlock['continuable'])) {\n                $methodName = 'block' . $CurrentBlock['type'] . 'Continue';\n                $Block = $this->$methodName($Line, $CurrentBlock);\n\n                if (isset($Block)) {\n                    $CurrentBlock = $Block;\n                    continue;\n                }\n                else {\n                    if ($this->isBlockCompletable($CurrentBlock['type'])) {\n                        $methodName = 'block' . $CurrentBlock['type'] . 'Complete';\n                        $CurrentBlock = $this->$methodName($CurrentBlock);\n                    }\n                }\n            }\n\n            $marker = $text[0];\n\n            $blockTypes = $this->unmarkedBlockTypes;\n\n            if (isset($this->BlockTypes[$marker])) {\n                foreach ($this->BlockTypes[$marker] as $blockType) {\n                    $blockTypes[] = $blockType;\n                }\n            }\n\n            foreach ($blockTypes as $blockType) {\n                $Block = $this->{\"block$blockType\"}($Line, $CurrentBlock);\n\n                if (isset($Block)) {\n                    $Block['type'] = $blockType;\n\n                    if (!isset($Block['identified'])) {\n                        if (isset($CurrentBlock)) {\n                            $Elements[] = $this->extractElement($CurrentBlock);\n                        }\n\n                        $Block['identified'] = true;\n                    }\n\n                    if ($this->isBlockContinuable($blockType)) {\n                        $Block['continuable'] = true;\n                    }\n\n                    $CurrentBlock = $Block;\n\n                    continue 2;\n                }\n            }\n\n            if (isset($CurrentBlock) && $CurrentBlock['type'] === 'Paragraph') {\n                $Block = $this->paragraphContinue($Line, $CurrentBlock);\n            }\n\n            if (isset($Block)) {\n                $CurrentBlock = $Block;\n            }\n            else {\n                if (isset($CurrentBlock)) {\n                    $Elements[] = $this->extractElement($CurrentBlock);\n                }\n\n                $CurrentBlock = $this->paragraph($Line);\n\n                $CurrentBlock['identified'] = true;\n            }\n        }\n\n        if (isset($CurrentBlock['continuable']) && $this->isBlockCompletable($CurrentBlock['type'])) {\n            $methodName = 'block' . $CurrentBlock['type'] . 'Complete';\n            $CurrentBlock = $this->$methodName($CurrentBlock);\n        }\n\n        if (isset($CurrentBlock)) {\n            $Elements[] = $this->extractElement($CurrentBlock);\n        }\n\n        return $Elements;\n    }\n\n    /**\n     * extractElement\n     */\n    protected function extractElement(array $Component)\n    {\n        if (!isset($Component['element'])) {\n            if (isset($Component['markup'])) {\n                $Component['element'] = array('rawHtml' => $Component['markup']);\n            }\n            elseif (isset($Component['hidden'])) {\n                $Component['element'] = [];\n            }\n        }\n\n        return $Component['element'];\n    }\n\n    /**\n     * isBlockContinuable\n     */\n    protected function isBlockContinuable($Type)\n    {\n        return method_exists($this, 'block' . $Type . 'Continue');\n    }\n\n    /**\n     * isBlockCompletable\n     */\n    protected function isBlockCompletable($Type)\n    {\n        return method_exists($this, 'block' . $Type . 'Complete');\n    }\n\n    /**\n     * blockCode\n     */\n    protected function blockCode($Line, $Block = null)\n    {\n        if (isset($Block) && $Block['type'] === 'Paragraph' && ! isset($Block['interrupted'])) {\n            return;\n        }\n\n        if ($Line['indent'] >= 4) {\n            $text = substr($Line['body'], 4);\n\n            $Block = array(\n                'element' => array(\n                    'name' => 'pre',\n                    'element' => array(\n                        'name' => 'code',\n                        'text' => $text,\n                    ),\n                ),\n            );\n\n            return $Block;\n        }\n    }\n\n    /**\n     * blockCodeContinue\n     */\n    protected function blockCodeContinue($Line, $Block)\n    {\n        if ($Line['indent'] >= 4) {\n            if (isset($Block['interrupted'])) {\n                $Block['element']['element']['text'] .= str_repeat(\"\\n\", $Block['interrupted']);\n\n                unset($Block['interrupted']);\n            }\n\n            $Block['element']['element']['text'] .= \"\\n\";\n\n            $text = substr($Line['body'], 4);\n\n            $Block['element']['element']['text'] .= $text;\n\n            return $Block;\n        }\n    }\n\n    /**\n     * blockCodeComplete\n     */\n    protected function blockCodeComplete($Block)\n    {\n        return $Block;\n    }\n\n    /**\n     * blockComment\n     */\n    protected function blockComment($Line)\n    {\n        if ($this->markupEscaped || $this->safeMode) {\n            return;\n        }\n\n        if (strpos($Line['text'], '<!--') === 0) {\n            $Block = array(\n                'element' => array(\n                    'rawHtml' => $Line['body'],\n                    'autobreak' => true,\n                ),\n            );\n\n            if (strpos($Line['text'], '-->') !== false) {\n                $Block['closed'] = true;\n            }\n\n            return $Block;\n        }\n    }\n\n    /**\n     * blockCommentContinue\n     */\n    protected function blockCommentContinue($Line, array $Block)\n    {\n        if (isset($Block['closed'])) {\n            return;\n        }\n\n        $Block['element']['rawHtml'] .= \"\\n\" . $Line['body'];\n\n        if (strpos($Line['text'], '-->') !== false) {\n            $Block['closed'] = true;\n        }\n\n        return $Block;\n    }\n\n    /**\n     * blockFencedCode\n     */\n    protected function blockFencedCode($Line)\n    {\n        $marker = $Line['text'][0];\n\n        $openerLength = strspn($Line['text'], $marker);\n\n        if ($openerLength < 3) {\n            return;\n        }\n\n        $infostring = trim(substr($Line['text'], $openerLength), \"\\t \");\n\n        if (strpos($infostring, '`') !== false) {\n            return;\n        }\n\n        $Element = array(\n            'name' => 'code',\n            'text' => '',\n        );\n\n        if ($infostring !== '') {\n            /**\n             * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes\n             * Every HTML element may have a class attribute specified.\n             * The attribute, if specified, must have a value that is a set\n             * of space-separated tokens representing the various classes\n             * that the element belongs to.\n             * [...]\n             * The space characters, for the purposes of this specification,\n             * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab),\n             * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and\n             * U+000D CARRIAGE RETURN (CR).\n             */\n            $language = substr($infostring, 0, strcspn($infostring, \" \\t\\n\\f\\r\"));\n\n            $Element['attributes'] = ['class' => \"language-$language\"];\n        }\n\n        $Block = [\n            'char' => $marker,\n            'openerLength' => $openerLength,\n            'element' => [\n                'name' => 'pre',\n                'element' => $Element,\n            ],\n        ];\n\n        return $Block;\n    }\n\n    /**\n     * blockFencedCodeContinue\n     */\n    protected function blockFencedCodeContinue($Line, $Block)\n    {\n        if (isset($Block['complete'])) {\n            return;\n        }\n\n        if (isset($Block['interrupted'])) {\n            $Block['element']['element']['text'] .= str_repeat(\"\\n\", $Block['interrupted']);\n\n            unset($Block['interrupted']);\n        }\n\n        if (\n            ($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength'] &&\n            chop(substr($Line['text'], $len), ' ') === ''\n        ) {\n            $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1);\n\n            $Block['complete'] = true;\n\n            return $Block;\n        }\n\n        $Block['element']['element']['text'] .= \"\\n\" . $Line['body'];\n\n        return $Block;\n    }\n\n    /**\n     * blockFencedCodeComplete\n     */\n    protected function blockFencedCodeComplete($Block)\n    {\n        return $Block;\n    }\n\n    /**\n     * blockHeader\n     */\n    protected function blockHeader($Line)\n    {\n        $level = strspn($Line['text'], '#');\n\n        if ($level > 6) {\n            return;\n        }\n\n        $text = trim($Line['text'], '#');\n\n        if ($this->strictMode && isset($text[0]) && $text[0] !== ' ') {\n            return;\n        }\n\n        $text = trim($text, ' ');\n\n        $Block = array(\n            'element' => array(\n                'name' => 'h' . $level,\n                'handler' => array(\n                    'function' => 'lineElements',\n                    'argument' => $text,\n                    'destination' => 'elements',\n                )\n            ),\n        );\n\n        return $Block;\n    }\n\n    /**\n     * blockList\n     */\n    protected function blockList($Line, ?array $CurrentBlock = null)\n    {\n        list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\\)]');\n\n        if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches)) {\n            $contentIndent = strlen($matches[2]);\n\n            if ($contentIndent >= 5) {\n                $contentIndent -= 1;\n                $matches[1] = substr($matches[1], 0, -$contentIndent);\n                $matches[3] = str_repeat(' ', $contentIndent) . $matches[3];\n            }\n            elseif ($contentIndent === 0) {\n                $matches[1] .= ' ';\n            }\n\n            $markerWithoutWhitespace = strstr($matches[1], ' ', true);\n\n            $Block = array(\n                'indent' => $Line['indent'],\n                'pattern' => $pattern,\n                'data' => array(\n                    'type' => $name,\n                    'marker' => $matches[1],\n                    'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)),\n                ),\n                'element' => array(\n                    'name' => $name,\n                    'elements' => [],\n                ),\n            );\n            $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/');\n\n            if ($name === 'ol') {\n                $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0';\n\n                if ($listStart !== '1') {\n                    if (\n                        isset($CurrentBlock)\n                        && $CurrentBlock['type'] === 'Paragraph'\n                        && ! isset($CurrentBlock['interrupted'])\n                    ) {\n                        return;\n                    }\n\n                    $Block['element']['attributes'] = array('start' => $listStart);\n                }\n            }\n\n            $Block['li'] = array(\n                'name' => 'li',\n                'handler' => array(\n                    'function' => 'li',\n                    'argument' => !empty($matches[3]) ? array($matches[3]) : [],\n                    'destination' => 'elements'\n                )\n            );\n\n            $Block['element']['elements'][] = & $Block['li'];\n\n            return $Block;\n        }\n    }\n\n    /**\n     * blockListContinue\n     */\n    protected function blockListContinue($Line, array $Block)\n    {\n        if (isset($Block['interrupted']) && empty($Block['li']['handler']['argument'])) {\n            return null;\n        }\n\n        $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker']));\n\n        if ($Line['indent'] < $requiredIndent && (\n                (\n                    $Block['data']['type'] === 'ol' &&\n                    preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches)\n                ) || (\n                    $Block['data']['type'] === 'ul' &&\n                    preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches)\n                )\n            )\n        ) {\n            if (isset($Block['interrupted'])) {\n                $Block['li']['handler']['argument'][] = '';\n\n                $Block['loose'] = true;\n\n                unset($Block['interrupted']);\n            }\n\n            unset($Block['li']);\n\n            $text = isset($matches[1]) ? $matches[1] : '';\n\n            $Block['indent'] = $Line['indent'];\n\n            $Block['li'] = array(\n                'name' => 'li',\n                'handler' => array(\n                    'function' => 'li',\n                    'argument' => array($text),\n                    'destination' => 'elements'\n                )\n            );\n\n            $Block['element']['elements'][] = & $Block['li'];\n\n            return $Block;\n        }\n        elseif ($Line['indent'] < $requiredIndent && $this->blockList($Line)) {\n            return null;\n        }\n\n        if ($Line['text'][0] === '[' && $this->blockReference($Line)) {\n            return $Block;\n        }\n\n        if ($Line['indent'] >= $requiredIndent) {\n            if (isset($Block['interrupted'])) {\n                $Block['li']['handler']['argument'][] = '';\n\n                $Block['loose'] = true;\n\n                unset($Block['interrupted']);\n            }\n\n            $text = substr($Line['body'], $requiredIndent);\n\n            $Block['li']['handler']['argument'][] = $text;\n\n            return $Block;\n        }\n\n        if (!isset($Block['interrupted'])) {\n            $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']);\n\n            $Block['li']['handler']['argument'][] = $text;\n\n            return $Block;\n        }\n    }\n\n    protected function blockListComplete(array $Block)\n    {\n        if (isset($Block['loose'])) {\n            foreach ($Block['element']['elements'] as &$li) {\n                if (end($li['handler']['argument']) !== '') {\n                    $li['handler']['argument'][] = '';\n                }\n            }\n        }\n\n        return $Block;\n    }\n\n    /**\n     * blockQuote\n     */\n    protected function blockQuote($Line)\n    {\n        if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) {\n            $Block = array(\n                'element' => array(\n                    'name' => 'blockquote',\n                    'handler' => array(\n                        'function' => 'linesElements',\n                        'argument' => (array) $matches[1],\n                        'destination' => 'elements',\n                    )\n                ),\n            );\n\n            return $Block;\n        }\n    }\n\n    /**\n     * blockQuoteContinue\n     */\n    protected function blockQuoteContinue($Line, array $Block)\n    {\n        if (isset($Block['interrupted'])) {\n            return;\n        }\n\n        if ($Line['text'][0] === '>' && preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) {\n            $Block['element']['handler']['argument'][] = $matches[1];\n\n            return $Block;\n        }\n\n        if (!isset($Block['interrupted'])) {\n            $Block['element']['handler']['argument'][] = $Line['text'];\n\n            return $Block;\n        }\n    }\n\n    /**\n     * blockRule\n     */\n    protected function blockRule($Line)\n    {\n        $marker = $Line['text'][0];\n\n        if (substr_count($Line['text'], $marker) >= 3 && chop($Line['text'], \" $marker\") === '') {\n            $Block = [\n                'element' => [\n                    'name' => 'hr',\n                ],\n            ];\n\n            return $Block;\n        }\n    }\n\n    /**\n     * blockSetextHeader\n     */\n    protected function blockSetextHeader($Line, ?array $Block = null)\n    {\n        if (!isset($Block) || $Block['type'] !== 'Paragraph' || isset($Block['interrupted'])) {\n            return;\n        }\n\n        if ($Line['indent'] < 4 && chop(chop($Line['text'], ' '), $Line['text'][0]) === '') {\n            $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';\n\n            return $Block;\n        }\n    }\n\n    /**\n     * blockMarkup\n     */\n    protected function blockMarkup($Line)\n    {\n        if ($this->markupEscaped || $this->safeMode) {\n            return;\n        }\n\n        if (preg_match('/^<[\\/]?+(\\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\\/)?>/', $Line['text'], $matches)) {\n            $element = strtolower($matches[1]);\n\n            if (in_array($element, $this->textLevelElements)) {\n                return;\n            }\n\n            $Block = array(\n                'name' => $matches[1],\n                'element' => array(\n                    'rawHtml' => $Line['text'],\n                    'autobreak' => true,\n                ),\n            );\n\n            return $Block;\n        }\n    }\n\n    /**\n     * blockMarkupContinue\n     */\n    protected function blockMarkupContinue($Line, array $Block)\n    {\n        if (isset($Block['closed']) || isset($Block['interrupted'])) {\n            return;\n        }\n\n        $Block['element']['rawHtml'] .= \"\\n\" . $Line['body'];\n\n        return $Block;\n    }\n\n    /**\n     * blockReference\n     */\n    protected function blockReference($Line)\n    {\n        if (strpos($Line['text'], ']') !== false\n            && preg_match('/^\\[(.+?)\\]:[ ]*+<?(\\S+?)>?(?:[ ]+[\"\\'(](.+)[\"\\')])?[ ]*+$/', $Line['text'], $matches)\n        ) {\n            $id = strtolower($matches[1]);\n\n            $Data = [\n                'url' => $matches[2],\n                'title' => isset($matches[3]) ? $matches[3] : null,\n            ];\n\n            $this->DefinitionData['Reference'][$id] = $Data;\n\n            $Block = [\n                'element' => [],\n            ];\n\n            return $Block;\n        }\n    }\n\n    /**\n     * blockTable\n     */\n    protected function blockTable($Line, ?array $Block = null)\n    {\n        if (!isset($Block) || $Block['type'] !== 'Paragraph' || isset($Block['interrupted'])) {\n            return;\n        }\n\n        if (\n            strpos($Block['element']['handler']['argument'], '|') === false &&\n            strpos($Line['text'], '|') === false &&\n            strpos($Line['text'], ':') === false ||\n            strpos($Block['element']['handler']['argument'], \"\\n\") !== false\n        ) {\n            return;\n        }\n\n        if (chop($Line['text'], ' -:|') !== '') {\n            return;\n        }\n\n        $alignments = [];\n\n        $divider = $Line['text'];\n\n        $divider = trim($divider);\n        $divider = trim($divider, '|');\n\n        $dividerCells = explode('|', $divider);\n\n        foreach ($dividerCells as $dividerCell) {\n            $dividerCell = trim($dividerCell);\n\n            if ($dividerCell === '') {\n                return;\n            }\n\n            $alignment = null;\n\n            if ($dividerCell[0] === ':') {\n                $alignment = 'left';\n            }\n\n            if (substr($dividerCell, - 1) === ':') {\n                $alignment = $alignment === 'left' ? 'center' : 'right';\n            }\n\n            $alignments[] = $alignment;\n        }\n\n        $HeaderElements = [];\n\n        $header = $Block['element']['handler']['argument'];\n\n        $header = trim($header);\n        $header = trim($header, '|');\n\n        $headerCells = explode('|', $header);\n\n        if (count($headerCells) !== count($alignments)) {\n            return;\n        }\n\n        foreach ($headerCells as $index => $headerCell) {\n            $headerCell = trim($headerCell);\n\n            $HeaderElement = array(\n                'name' => 'th',\n                'handler' => array(\n                    'function' => 'lineElements',\n                    'argument' => $headerCell,\n                    'destination' => 'elements',\n                )\n            );\n\n            if (isset($alignments[$index])) {\n                $alignment = $alignments[$index];\n\n                $HeaderElement['attributes'] = array(\n                    'style' => \"text-align: $alignment;\",\n                );\n            }\n\n            $HeaderElements[] = $HeaderElement;\n        }\n\n        $Block = [\n            'alignments' => $alignments,\n            'identified' => true,\n            'element' => [\n                'name' => 'table',\n                'elements' => [],\n            ],\n        ];\n\n        $Block['element']['elements'][] = [\n            'name' => 'thead',\n        ];\n\n        $Block['element']['elements'][] = [\n            'name' => 'tbody',\n            'elements' => [],\n        ];\n\n        $Block['element']['elements'][0]['elements'][] = [\n            'name' => 'tr',\n            'elements' => $HeaderElements,\n        ];\n\n        return $Block;\n    }\n\n    /**\n     * blockTableContinue\n     */\n    protected function blockTableContinue($Line, array $Block)\n    {\n        if (isset($Block['interrupted'])) {\n            return;\n        }\n\n        if (count($Block['alignments']) === 1 || $Line['text'][0] === '|' || strpos($Line['text'], '|')) {\n            $Elements = [];\n\n            $row = $Line['text'];\n\n            $row = trim($row);\n            $row = trim($row, '|');\n\n            preg_match_all('/(?:(\\\\\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches);\n\n            $cells = array_slice($matches[0], 0, count($Block['alignments']));\n\n            foreach ($cells as $index => $cell) {\n                $cell = trim($cell);\n\n                $Element = array(\n                    'name' => 'td',\n                    'handler' => array(\n                        'function' => 'lineElements',\n                        'argument' => $cell,\n                        'destination' => 'elements',\n                    )\n                );\n\n                if (isset($Block['alignments'][$index])) {\n                    $Element['attributes'] = array(\n                        'style' => 'text-align: ' . $Block['alignments'][$index] . ';',\n                    );\n                }\n\n                $Elements[] = $Element;\n            }\n\n            $Element = array(\n                'name' => 'tr',\n                'elements' => $Elements,\n            );\n\n            $Block['element']['elements'][1]['elements'][] = $Element;\n\n            return $Block;\n        }\n    }\n\n    /**\n     * paragraph\n     */\n    protected function paragraph($Line)\n    {\n        return array(\n            'type' => 'Paragraph',\n            'element' => array(\n                'name' => 'p',\n                'handler' => array(\n                    'function' => 'lineElements',\n                    'argument' => $Line['text'],\n                    'destination' => 'elements',\n                ),\n            ),\n        );\n    }\n\n    /**\n     * paragraphContinue\n     */\n    protected function paragraphContinue($Line, array $Block)\n    {\n        if (isset($Block['interrupted'])) {\n            return;\n        }\n\n        $Block['element']['handler']['argument'] .= \"\\n\".$Line['text'];\n\n        return $Block;\n    }\n\n    /**\n     * line\n     */\n    public function line($text, $nonNestables = [])\n    {\n        return $this->elements($this->lineElements($text, $nonNestables));\n    }\n\n    /**\n     * lineElements\n     */\n    protected function lineElements($text, $nonNestables = [])\n    {\n        // standardize line breaks\n        $text = str_replace(array(\"\\r\\n\", \"\\r\"), \"\\n\", $text);\n\n        $Elements = [];\n\n        $nonNestables = (empty($nonNestables)\n            ? []\n            : array_combine($nonNestables, $nonNestables)\n        );\n\n        // $excerpt is based on the first occurrence of a marker\n\n        while ($excerpt = strpbrk($text, $this->inlineMarkerList)) {\n            $marker = $excerpt[0];\n\n            $markerPosition = strlen($text) - strlen($excerpt);\n\n            $Excerpt = ['text' => $excerpt, 'context' => $text];\n\n            foreach ($this->InlineTypes[$marker] as $inlineType) {\n                // Check to see if the current inline type is nestable in the current context\n                if (isset($nonNestables[$inlineType])) {\n                    continue;\n                }\n\n                $Inline = $this->{\"inline$inlineType\"}($Excerpt);\n                if (!isset($Inline)) {\n                    continue;\n                }\n\n                // Make sure that the inline belongs to \"our\" marker\n                if (isset($Inline['position']) && $Inline['position'] > $markerPosition) {\n                    continue;\n                }\n\n                // Set a default inline position\n                if (!isset($Inline['position'])) {\n                    $Inline['position'] = $markerPosition;\n                }\n\n                // Cause the new element to 'inherit' our non nestables\n                $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables'])\n                    ? array_merge($Inline['element']['nonNestables'], $nonNestables)\n                    : $nonNestables\n                ;\n\n                // The text that comes before the inline\n                $unmarkedText = substr($text, 0, $Inline['position']);\n\n                // Compile the unmarked text\n                $InlineText = $this->inlineText($unmarkedText);\n                $Elements[] = $InlineText['element'];\n\n                // Compile the inline\n                $Elements[] = $this->extractElement($Inline);\n\n                // Remove the examined text\n                $text = substr($text, $Inline['position'] + $Inline['extent']);\n\n                continue 2;\n            }\n\n            // The marker does not belong to an inline\n            $unmarkedText = substr($text, 0, $markerPosition + 1);\n\n            $InlineText = $this->inlineText($unmarkedText);\n            $Elements[] = $InlineText['element'];\n\n            $text = substr($text, $markerPosition + 1);\n        }\n\n        $InlineText = $this->inlineText($text);\n        $Elements[] = $InlineText['element'];\n\n        foreach ($Elements as &$Element) {\n            if (!isset($Element['autobreak'])) {\n                $Element['autobreak'] = false;\n            }\n        }\n\n        return $Elements;\n    }\n\n    /**\n     * inlineText\n     */\n    protected function inlineText($text)\n    {\n        $Inline = [\n            'extent' => strlen($text),\n            'element' => [],\n        ];\n\n        $Inline['element']['elements'] = self::pregReplaceElements(\n            $this->breaksEnabled ? '/[ ]*+\\n/' : '/(?:[ ]*+\\\\\\\\|[ ]{2,}+)\\n/',\n            [\n                ['name' => 'br'],\n                ['text' => \"\\n\"],\n            ],\n            $text\n        );\n\n        return $Inline;\n    }\n\n    /**\n     * inlineCode\n     */\n    protected function inlineCode($Excerpt)\n    {\n        $marker = $Excerpt['text'][0];\n\n        if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(?<!['.$marker.'])\\1(?!'.$marker.')/s', $Excerpt['text'], $matches)) {\n            $text = $matches[2];\n            $text = preg_replace('/[ ]*+\\n/', ' ', $text);\n\n            return [\n                'extent' => strlen($matches[0]),\n                'element' => [\n                    'name' => 'code',\n                    'text' => $text,\n                ],\n            ];\n        }\n    }\n\n    /**\n     * inlineEmailTag\n     */\n    protected function inlineEmailTag($Excerpt)\n    {\n        $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?';\n\n        $commonMarkEmail = '[a-zA-Z0-9.!#$%&\\'*+\\/=?^_`{|}~-]++@'\n            . $hostnameLabel . '(?:\\.' . $hostnameLabel . ')*';\n\n        if (\n            strpos($Excerpt['text'], '>') !== false &&\n            preg_match(\"/^<((mailto:)?$commonMarkEmail)>/i\", $Excerpt['text'], $matches)\n        ){\n            $url = $matches[1];\n\n            if (!isset($matches[2])) {\n                $url = \"mailto:$url\";\n            }\n\n            return [\n                'extent' => strlen($matches[0]),\n                'element' => [\n                    'name' => 'a',\n                    'text' => $matches[1],\n                    'attributes' => [\n                        'href' => $url,\n                    ],\n                ],\n            ];\n        }\n    }\n\n    /**\n     * inlineEmphasis\n     */\n    protected function inlineEmphasis($Excerpt)\n    {\n        if (!isset($Excerpt['text'][1])) {\n            return;\n        }\n\n        $marker = $Excerpt['text'][0];\n\n        if ($Excerpt['text'][1] === $marker && preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) {\n            $emphasis = 'strong';\n        }\n        elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) {\n            $emphasis = 'em';\n        }\n        else {\n            return;\n        }\n\n        return [\n            'extent' => strlen($matches[0]),\n            'element' => [\n                'name' => $emphasis,\n                'handler' => [\n                    'function' => 'lineElements',\n                    'argument' => $matches[1],\n                    'destination' => 'elements',\n                ]\n            ],\n        ];\n    }\n\n    /**\n     * inlineEscapeSequence\n     */\n    protected function inlineEscapeSequence($Excerpt)\n    {\n        if (isset($Excerpt['text'][1]) && in_array($Excerpt['text'][1], $this->specialCharacters)) {\n            return array(\n                'element' => array('rawHtml' => $Excerpt['text'][1]),\n                'extent' => 2,\n            );\n        }\n    }\n\n    /**\n     * inlineImage\n     */\n    protected function inlineImage($Excerpt)\n    {\n        if (!isset($Excerpt['text'][1]) || $Excerpt['text'][1] !== '[') {\n            return;\n        }\n\n        $Excerpt['text']= substr($Excerpt['text'], 1);\n\n        $Link = $this->inlineLink($Excerpt);\n\n        if ($Link === null) {\n            return;\n        }\n\n        $Inline = array(\n            'extent' => $Link['extent'] + 1,\n            'element' => array(\n                'name' => 'img',\n                'attributes' => array(\n                    'src' => $Link['element']['attributes']['href'],\n                    'alt' => $Link['element']['handler']['argument'],\n                ),\n                'autobreak' => true,\n            ),\n        );\n\n        $Inline['element']['attributes'] += $Link['element']['attributes'];\n\n        unset($Inline['element']['attributes']['href']);\n\n        return $Inline;\n    }\n\n    /**\n     * inlineLink\n     */\n    protected function inlineLink($Excerpt)\n    {\n        $Element = array(\n            'name' => 'a',\n            'handler' => array(\n                'function' => 'lineElements',\n                'argument' => null,\n                'destination' => 'elements',\n            ),\n            'nonNestables' => array('Url', 'Link'),\n            'attributes' => array(\n                'href' => null,\n                'title' => null,\n            ),\n        );\n\n        $extent = 0;\n\n        $remainder = $Excerpt['text'];\n\n        if (preg_match('/\\[((?:[^][]++|(?R))*+)\\]/', $remainder, $matches)) {\n            $Element['handler']['argument'] = $matches[1];\n\n            $extent += strlen($matches[0]);\n\n            $remainder = substr($remainder, $extent);\n        }\n        else {\n            return;\n        }\n\n        if (preg_match('/^[(]\\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+(\"[^\"]*+\"|\\'[^\\']*+\\'))?\\s*+[)]/', $remainder, $matches)) {\n            $Element['attributes']['href'] = $matches[1];\n\n            if (isset($matches[2])) {\n                $Element['attributes']['title'] = substr($matches[2], 1, - 1);\n            }\n\n            $extent += strlen($matches[0]);\n        }\n        else {\n            if (preg_match('/^\\s*\\[(.*?)\\]/', $remainder, $matches)) {\n                $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument'];\n                $definition = strtolower($definition);\n\n                $extent += strlen($matches[0]);\n            }\n            else {\n                $definition = strtolower($Element['handler']['argument']);\n            }\n\n            if (!isset($this->DefinitionData['Reference'][$definition])) {\n                return;\n            }\n\n            $Definition = $this->DefinitionData['Reference'][$definition];\n\n            $Element['attributes']['href'] = $Definition['url'];\n            $Element['attributes']['title'] = $Definition['title'];\n        }\n\n        return [\n            'extent' => $extent,\n            'element' => $Element,\n        ];\n    }\n\n    /**\n     * inlineMarkup\n     */\n    protected function inlineMarkup($Excerpt)\n    {\n        if ($this->markupEscaped || $this->safeMode || strpos($Excerpt['text'], '>') === false) {\n            return;\n        }\n\n        if ($Excerpt['text'][1] === '/' && preg_match('/^<\\/\\w[\\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) {\n            return array(\n                'element' => array('rawHtml' => $matches[0]),\n                'extent' => strlen($matches[0]),\n            );\n        }\n\n        if ($Excerpt['text'][1] === '!' && preg_match('/^<!---?[^>-](?:-?+[^-])*-->/s', $Excerpt['text'], $matches)) {\n            return array(\n                'element' => array('rawHtml' => $matches[0]),\n                'extent' => strlen($matches[0]),\n            );\n        }\n\n        if ($Excerpt['text'][1] !== ' ' && preg_match('/^<\\w[\\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\\/?>/s', $Excerpt['text'], $matches)) {\n            return array(\n                'element' => array('rawHtml' => $matches[0]),\n                'extent' => strlen($matches[0]),\n            );\n        }\n    }\n\n    /**\n     * inlineSpecialCharacter\n     */\n    protected function inlineSpecialCharacter($Excerpt)\n    {\n        if (\n            substr($Excerpt['text'], 1, 1) !== ' ' && strpos($Excerpt['text'], ';') !== false &&\n            preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches)\n        ) {\n            return array(\n                'element' => array('rawHtml' => '&' . $matches[1] . ';'),\n                'extent' => strlen($matches[0]),\n            );\n        }\n\n        return;\n    }\n\n    /**\n     * inlineStrikethrough\n     */\n    protected function inlineStrikethrough($Excerpt)\n    {\n        if (!isset($Excerpt['text'][1])) {\n            return;\n        }\n\n        if ($Excerpt['text'][1] === '~' && preg_match('/^~~(?=\\S)(.+?)(?<=\\S)~~/', $Excerpt['text'], $matches)) {\n            return array(\n                'extent' => strlen($matches[0]),\n                'element' => array(\n                    'name' => 'del',\n                    'handler' => array(\n                        'function' => 'lineElements',\n                        'argument' => $matches[1],\n                        'destination' => 'elements',\n                    )\n                ),\n            );\n        }\n    }\n\n    /**\n     * inlineUrl\n     */\n    protected function inlineUrl($Excerpt)\n    {\n        if ($this->urlsLinked !== true || ! isset($Excerpt['text'][2]) || $Excerpt['text'][2] !== '/') {\n            return;\n        }\n\n        if (strpos($Excerpt['context'], 'http') !== false\n            && preg_match('/\\bhttps?+:[\\/]{2}[^\\s<]+\\b\\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)\n        ) {\n            $url = $matches[0][0];\n\n            $Inline = array(\n                'extent' => strlen($matches[0][0]),\n                'position' => $matches[0][1],\n                'element' => array(\n                    'name' => 'a',\n                    'text' => $url,\n                    'attributes' => array(\n                        'href' => $url,\n                    ),\n                ),\n            );\n\n            return $Inline;\n        }\n    }\n\n    /**\n     * inlineUrlTag\n     */\n    protected function inlineUrlTag($Excerpt)\n    {\n        if (strpos($Excerpt['text'], '>') !== false && preg_match('/^<(\\w++:\\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) {\n            $url = $matches[1];\n\n            return array(\n                'extent' => strlen($matches[0]),\n                'element' => array(\n                    'name' => 'a',\n                    'text' => $url,\n                    'attributes' => array(\n                        'href' => $url,\n                    ),\n                ),\n            );\n        }\n    }\n\n    /**\n     * unmarkedText\n     */\n    protected function unmarkedText($text)\n    {\n        $Inline = $this->inlineText($text);\n        return $this->element($Inline['element']);\n    }\n\n    /**\n     * handle\n     */\n    protected function handle(array $Element)\n    {\n        if (isset($Element['handler'])) {\n            if (!isset($Element['nonNestables'])) {\n                $Element['nonNestables'] = [];\n            }\n\n            if (is_string($Element['handler'])) {\n                $function = $Element['handler'];\n                $argument = $Element['text'];\n                unset($Element['text']);\n                $destination = 'rawHtml';\n            }\n            else {\n                $function = $Element['handler']['function'];\n                $argument = $Element['handler']['argument'];\n                $destination = $Element['handler']['destination'];\n            }\n\n            $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']);\n\n            if ($destination === 'handler') {\n                $Element = $this->handle($Element);\n            }\n\n            unset($Element['handler']);\n        }\n\n        return $Element;\n    }\n\n    /**\n     * handleElementRecursive\n     */\n    protected function handleElementRecursive(array $Element)\n    {\n        return $this->elementApplyRecursive(array($this, 'handle'), $Element);\n    }\n\n    /**\n     * handleElementsRecursive\n     */\n    protected function handleElementsRecursive(array $Elements)\n    {\n        return $this->elementsApplyRecursive(array($this, 'handle'), $Elements);\n    }\n\n    /**\n     * elementApplyRecursive\n     */\n    protected function elementApplyRecursive($closure, array $Element)\n    {\n        $Element = call_user_func($closure, $Element);\n\n        if (isset($Element['elements'])) {\n            $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']);\n        }\n        elseif (isset($Element['element'])) {\n            $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']);\n        }\n\n        return $Element;\n    }\n\n    /**\n     * elementApplyRecursiveDepthFirst\n     */\n    protected function elementApplyRecursiveDepthFirst($closure, array $Element)\n    {\n        if (isset($Element['elements'])) {\n            $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']);\n        }\n        elseif (isset($Element['element'])) {\n            $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']);\n        }\n\n        $Element = call_user_func($closure, $Element);\n\n        return $Element;\n    }\n\n    /**\n     * elementsApplyRecursive\n     */\n    protected function elementsApplyRecursive($closure, array $Elements)\n    {\n        foreach ($Elements as &$Element) {\n            $Element = $this->elementApplyRecursive($closure, $Element);\n        }\n\n        return $Elements;\n    }\n\n    /**\n     * elementsApplyRecursiveDepthFirst\n     */\n    protected function elementsApplyRecursiveDepthFirst($closure, array $Elements)\n    {\n        foreach ($Elements as &$Element) {\n            $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element);\n        }\n\n        return $Elements;\n    }\n\n    /**\n     * element\n     */\n    protected function element(array $Element)\n    {\n        if ($this->safeMode) {\n            $Element = $this->sanitiseElement($Element);\n        }\n\n        // identity map if element has no handler\n        $Element = $this->handle($Element);\n\n        $hasName = isset($Element['name']);\n\n        $markup = '';\n\n        if ($hasName) {\n            $markup .= '<' . $Element['name'];\n\n            if (isset($Element['attributes'])) {\n                foreach ($Element['attributes'] as $name => $value) {\n                    if ($value === null)\n                    {\n                        continue;\n                    }\n\n                    $markup .= \" $name=\\\"\".self::escape($value).'\"';\n                }\n            }\n        }\n\n        $permitRawHtml = false;\n\n        if (isset($Element['text'])) {\n            $text = $Element['text'];\n        }\n        // Very strongly consider an alternative if you're writing an extension\n        elseif (isset($Element['rawHtml'])) {\n            $text = $Element['rawHtml'];\n\n            $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode'];\n            $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode;\n        }\n\n        $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']);\n\n        if ($hasContent) {\n            $markup .= $hasName ? '>' : '';\n\n            if (isset($Element['elements'])) {\n                $markup .= $this->elements($Element['elements']);\n            }\n            elseif (isset($Element['element'])) {\n                $markup .= $this->element($Element['element']);\n            }\n            else {\n                if (!$permitRawHtml) {\n                    $markup .= self::escape($text, true);\n                }\n                else {\n                    $markup .= $text;\n                }\n            }\n\n            $markup .= $hasName ? '</' . $Element['name'] . '>' : '';\n        }\n        elseif ($hasName) {\n            $markup .= ' />';\n        }\n\n        return $markup;\n    }\n\n    /**\n     * elements\n     */\n    protected function elements(array $Elements)\n    {\n        $markup = '';\n\n        $autoBreak = true;\n\n        foreach ($Elements as $Element) {\n            if (empty($Element)) {\n                continue;\n            }\n\n            $autoBreakNext = (isset($Element['autobreak'])\n                ? $Element['autobreak'] : isset($Element['name'])\n            );\n            // (autobreak === false) covers both sides of an element\n            $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext;\n\n            $markup .= ($autoBreak ? \"\\n\" : '') . $this->element($Element);\n            $autoBreak = $autoBreakNext;\n        }\n\n        $markup .= $autoBreak ? \"\\n\" : '';\n\n        return $markup;\n    }\n\n    /**\n     * li\n     */\n    protected function li($lines)\n    {\n        $Elements = $this->linesElements($lines);\n\n        if (!in_array('', $lines)\n            && isset($Elements[0]) && isset($Elements[0]['name'])\n            && $Elements[0]['name'] === 'p'\n        ) {\n            unset($Elements[0]['name']);\n        }\n\n        return $Elements;\n    }\n\n    //\n    // AST Convenience\n    //\n\n    /**\n     * Replace occurrences $regexp with $Elements in $text. Return an array of\n     * elements representing the replacement.\n     */\n    protected static function pregReplaceElements($regexp, $Elements, $text)\n    {\n        $newElements = [];\n\n        while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) {\n            $offset = $matches[0][1];\n            $before = substr($text, 0, $offset);\n            $after = substr($text, $offset + strlen($matches[0][0]));\n\n            $newElements[] = array('text' => $before);\n\n            foreach ($Elements as $Element) {\n                $newElements[] = $Element;\n            }\n\n            $text = $after;\n        }\n\n        $newElements[] = array('text' => $text);\n\n        return $newElements;\n    }\n\n    //\n    // Deprecated Methods\n    //\n\n    /**\n     * parse\n     */\n    function parse($text)\n    {\n        $markup = $this->text($text);\n\n        return $markup;\n    }\n\n    /**\n     * sanitiseElement\n     */\n    protected function sanitiseElement(array $Element)\n    {\n        static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';\n        static $safeUrlNameToAtt  = array(\n            'a'   => 'href',\n            'img' => 'src',\n        );\n\n        if (!isset($Element['name'])) {\n            unset($Element['attributes']);\n            return $Element;\n        }\n\n        if (isset($safeUrlNameToAtt[$Element['name']])) {\n            $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]);\n        }\n\n        if (!empty($Element['attributes'])) {\n            foreach ($Element['attributes'] as $att => $val) {\n                // filter out badly parsed attribute\n                if (!preg_match($goodAttribute, $att)) {\n                    unset($Element['attributes'][$att]);\n                }\n                // dump onevent attribute\n                elseif (self::striAtStart($att, 'on')) {\n                    unset($Element['attributes'][$att]);\n                }\n            }\n        }\n\n        return $Element;\n    }\n\n    /**\n     * filterUnsafeUrlInAttribute\n     */\n    protected function filterUnsafeUrlInAttribute(array $Element, $attribute)\n    {\n        foreach ($this->safeLinksWhitelist as $scheme) {\n            if (self::striAtStart($Element['attributes'][$attribute], $scheme)) {\n                return $Element;\n            }\n        }\n\n        $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]);\n\n        return $Element;\n    }\n\n    /**\n     * escape\n     */\n    protected static function escape($text, $allowQuotes = false)\n    {\n        return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8');\n    }\n\n    /**\n     * striAtStart\n     */\n    protected static function striAtStart($string, $needle)\n    {\n        $len = strlen($needle);\n\n        if ($len > strlen($string)) {\n            return false;\n        }\n        else {\n            return strtolower(substr($string, 0, $len)) === strtolower($needle);\n        }\n    }\n\n    /**\n     * instance\n     */\n    static function instance($name = 'default')\n    {\n        if (isset(self::$instances[$name])) {\n            return self::$instances[$name];\n        }\n\n        $instance = new static();\n\n        self::$instances[$name] = $instance;\n\n        return $instance;\n    }\n}\n"
  },
  {
    "path": "src/Parse/Parsedown/ParsedownExtra.php",
    "content": "<?php namespace October\\Rain\\Parse\\Parsedown;\n\nuse DOMElement;\nuse DOMDocument;\n\n/**\n * ParsedownExtra\n */\nclass ParsedownExtra extends Parsedown\n{\n    /**\n     * @var int footnoteCount\n     */\n    protected $footnoteCount = 0;\n\n    /**\n     * @var mixed currentAbreviation\n     */\n    protected $currentAbreviation;\n\n    /**\n     * @var mixed currentMeaning\n     */\n    protected $currentMeaning;\n\n    /**\n     * @var string regexAttribute\n     */\n    protected $regexAttribute = '(?:[#.][-\\w]+[ ]*)';\n\n    /**\n     * __construct\n     */\n    public function __construct()\n    {\n        $this->BlockTypes[':'][] = 'DefinitionList';\n        $this->BlockTypes['*'][] = 'Abbreviation';\n\n        // identify footnote definitions before reference definitions\n        array_unshift($this->BlockTypes['['], 'Footnote');\n\n        // identify footnote markers before before links\n        array_unshift($this->InlineTypes['['], 'FootnoteMarker');\n    }\n\n    /**\n     * text\n     */\n    public function text($text)\n    {\n        $Elements = $this->textElements($text);\n\n        // convert to markup\n        $markup = $this->elements($Elements);\n\n        // trim line breaks\n        $markup = trim($markup, \"\\n\");\n\n        // merge consecutive dl elements\n\n        $markup = preg_replace('/<\\/dl>\\s+<dl>\\s+/', '', $markup);\n\n        // add footnotes\n\n        if (isset($this->DefinitionData['Footnote']))\n        {\n            $Element = $this->buildFootnoteElement();\n\n            $markup .= \"\\n\" . $this->element($Element);\n        }\n\n        return $markup;\n    }\n\n    /**\n     * blockAbbreviation\n     */\n    protected function blockAbbreviation($Line)\n    {\n        if (preg_match('/^\\*\\[(.+?)\\]:[ ]*(.+?)[ ]*$/', $Line['text'], $matches))\n        {\n            $this->DefinitionData['Abbreviation'][$matches[1]] = $matches[2];\n\n            $Block = array(\n                'hidden' => true,\n            );\n\n            return $Block;\n        }\n    }\n\n    /**\n     * blockFootnote\n     */\n    protected function blockFootnote($Line)\n    {\n        if (preg_match('/^\\[\\^(.+?)\\]:[ ]?(.*)$/', $Line['text'], $matches))\n        {\n            $Block = array(\n                'label' => $matches[1],\n                'text' => $matches[2],\n                'hidden' => true,\n            );\n\n            return $Block;\n        }\n    }\n\n    /**\n     * blockFootnoteContinue\n     */\n    protected function blockFootnoteContinue($Line, $Block)\n    {\n        if ($Line['text'][0] === '[' && preg_match('/^\\[\\^(.+?)\\]:/', $Line['text']))\n        {\n            return;\n        }\n\n        if (isset($Block['interrupted']))\n        {\n            if ($Line['indent'] >= 4)\n            {\n                $Block['text'] .= \"\\n\\n\" . $Line['text'];\n\n                return $Block;\n            }\n        }\n        else\n        {\n            $Block['text'] .= \"\\n\" . $Line['text'];\n\n            return $Block;\n        }\n    }\n\n    /**\n     * blockFootnoteComplete\n     */\n    protected function blockFootnoteComplete($Block)\n    {\n        $this->DefinitionData['Footnote'][$Block['label']] = array(\n            'text' => $Block['text'],\n            'count' => null,\n            'number' => null,\n        );\n\n        return $Block;\n    }\n\n    /**\n     * blockDefinitionList\n     */\n    protected function blockDefinitionList($Line, $Block)\n    {\n        if (!isset($Block) || $Block['type'] !== 'Paragraph')\n        {\n            return;\n        }\n\n        $Element = array(\n            'name' => 'dl',\n            'elements' => [],\n        );\n\n        $terms = explode(\"\\n\", $Block['element']['handler']['argument']);\n\n        foreach ($terms as $term)\n        {\n            $Element['elements'] []= array(\n                'name' => 'dt',\n                'handler' => array(\n                    'function' => 'lineElements',\n                    'argument' => $term,\n                    'destination' => 'elements'\n                ),\n            );\n        }\n\n        $Block['element'] = $Element;\n\n        $Block = $this->addDdElement($Line, $Block);\n\n        return $Block;\n    }\n\n    /**\n     * blockDefinitionListContinue\n     */\n    protected function blockDefinitionListContinue($Line, array $Block)\n    {\n        if ($Line['text'][0] === ':')\n        {\n            $Block = $this->addDdElement($Line, $Block);\n\n            return $Block;\n        }\n        else\n        {\n            if (isset($Block['interrupted']) && $Line['indent'] === 0)\n            {\n                return;\n            }\n\n            if (isset($Block['interrupted']))\n            {\n                $Block['dd']['handler']['function'] = 'textElements';\n                $Block['dd']['handler']['argument'] .= \"\\n\\n\";\n\n                $Block['dd']['handler']['destination'] = 'elements';\n\n                unset($Block['interrupted']);\n            }\n\n            $text = substr($Line['body'], min($Line['indent'], 4));\n\n            $Block['dd']['handler']['argument'] .= \"\\n\" . $text;\n\n            return $Block;\n        }\n    }\n\n    /**\n     * blockHeader\n     */\n    protected function blockHeader($Line)\n    {\n        $Block = parent::blockHeader($Line);\n\n        if ($Block !== null && preg_match('/[ #]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE))\n        {\n            $attributeString = $matches[1][0];\n\n            $Block['element']['attributes'] = $this->parseAttributeData($attributeString);\n\n            $Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]);\n        }\n\n        return $Block;\n    }\n\n    /**\n     * blockMarkup\n     */\n    protected function blockMarkup($Line)\n    {\n        if ($this->markupEscaped || $this->safeMode)\n        {\n            return;\n        }\n\n        if (preg_match('/^<(\\w[\\w-]*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\\/)?>/', $Line['text'], $matches))\n        {\n            $element = strtolower($matches[1]);\n\n            if (in_array($element, $this->textLevelElements))\n            {\n                return;\n            }\n\n            $Block = array(\n                'name' => $matches[1],\n                'depth' => 0,\n                'element' => array(\n                    'rawHtml' => $Line['text'],\n                    'autobreak' => true,\n                ),\n            );\n\n            $length = strlen($matches[0]);\n            $remainder = substr($Line['text'], $length);\n\n            if (trim($remainder) === '')\n            {\n                if (isset($matches[2]) || in_array($matches[1], $this->voidElements))\n                {\n                    $Block['closed'] = true;\n                    $Block['void'] = true;\n                }\n            }\n            else\n            {\n                if (isset($matches[2]) || in_array($matches[1], $this->voidElements))\n                {\n                    return;\n                }\n                if (preg_match('/<\\/'.$matches[1].'>[ ]*$/i', $remainder))\n                {\n                    $Block['closed'] = true;\n                }\n            }\n\n            return $Block;\n        }\n    }\n\n    /**\n     * blockMarkupContinue\n     */\n    protected function blockMarkupContinue($Line, array $Block)\n    {\n        if (isset($Block['closed'])) {\n            return;\n        }\n\n        // Open\n        if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) {\n            $Block['depth'] ++;\n        }\n\n        // Close\n        if (preg_match('/(.*?)<\\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) {\n            if ($Block['depth'] > 0) {\n                $Block['depth'] --;\n            }\n            else {\n                $Block['closed'] = true;\n            }\n        }\n\n        if (isset($Block['interrupted'])) {\n            $Block['element']['rawHtml'] .= \"\\n\";\n            unset($Block['interrupted']);\n        }\n\n        $Block['element']['rawHtml'] .= \"\\n\".$Line['body'];\n\n        return $Block;\n    }\n\n    /**\n     * blockMarkupComplete\n     */\n    protected function blockMarkupComplete($Block)\n    {\n        if (!isset($Block['void']))\n        {\n            $Block['element']['rawHtml'] = $this->processTag($Block['element']['rawHtml']);\n        }\n\n        return $Block;\n    }\n\n    /**\n     * blockSetextHeader\n     */\n    protected function blockSetextHeader($Line, ?array $Block = null)\n    {\n        $Block = parent::blockSetextHeader($Line, $Block);\n\n        if ($Block !== null && preg_match('/[ ]*{('.$this->regexAttribute.'+)}[ ]*$/', $Block['element']['handler']['argument'], $matches, PREG_OFFSET_CAPTURE))\n        {\n            $attributeString = $matches[1][0];\n\n            $Block['element']['attributes'] = $this->parseAttributeData($attributeString);\n\n            $Block['element']['handler']['argument'] = substr($Block['element']['handler']['argument'], 0, $matches[0][1]);\n        }\n\n        return $Block;\n    }\n\n    /**\n     * inlineFootnoteMarker\n     */\n    protected function inlineFootnoteMarker($Excerpt)\n    {\n        if (preg_match('/^\\[\\^(.+?)\\]/', $Excerpt['text'], $matches)) {\n            $name = $matches[1];\n\n            if (!isset($this->DefinitionData['Footnote'][$name])) {\n                return;\n            }\n\n            $this->DefinitionData['Footnote'][$name]['count'] ++;\n\n            if (!isset($this->DefinitionData['Footnote'][$name]['number'])) {\n                // » &\n                $this->DefinitionData['Footnote'][$name]['number'] = ++ $this->footnoteCount;\n            }\n\n            $Element = [\n                'name' => 'sup',\n                'attributes' => ['id' => 'fnref'.$this->DefinitionData['Footnote'][$name]['count'].':'.$name],\n                'element' => [\n                    'name' => 'a',\n                    'attributes' => ['href' => '#fn:'.$name, 'class' => 'footnote-ref'],\n                    'text' => $this->DefinitionData['Footnote'][$name]['number'],\n                ],\n            ];\n\n            return [\n                'extent' => strlen($matches[0]),\n                'element' => $Element,\n            ];\n        }\n    }\n\n    /**\n     * inlineLink\n     */\n    protected function inlineLink($Excerpt)\n    {\n        $Link = parent::inlineLink($Excerpt);\n\n        $remainder = $Link !== null ? substr($Excerpt['text'], $Link['extent']) : '';\n\n        if (preg_match('/^[ ]*{('.$this->regexAttribute.'+)}/', $remainder, $matches)) {\n            $Link['element']['attributes'] += $this->parseAttributeData($matches[1]);\n\n            $Link['extent'] += strlen($matches[0]);\n        }\n\n        return $Link;\n    }\n\n    /**\n     * insertAbreviation\n     */\n    protected function insertAbreviation(array $Element)\n    {\n        if (isset($Element['text'])) {\n            $Element['elements'] = self::pregReplaceElements(\n                '/\\b'.preg_quote($this->currentAbreviation, '/').'\\b/',\n                array(\n                    array(\n                        'name' => 'abbr',\n                        'attributes' => array(\n                            'title' => $this->currentMeaning,\n                        ),\n                        'text' => $this->currentAbreviation,\n                    )\n                ),\n                $Element['text']\n            );\n\n            unset($Element['text']);\n        }\n\n        return $Element;\n    }\n\n    /**\n     * inlineText\n     */\n    protected function inlineText($text)\n    {\n        $Inline = parent::inlineText($text);\n\n        if (isset($this->DefinitionData['Abbreviation']))\n        {\n            foreach ($this->DefinitionData['Abbreviation'] as $abbreviation => $meaning)\n            {\n                $this->currentAbreviation = $abbreviation;\n                $this->currentMeaning = $meaning;\n\n                $Inline['element'] = $this->elementApplyRecursiveDepthFirst(\n                    array($this, 'insertAbreviation'),\n                    $Inline['element']\n                );\n            }\n        }\n\n        return $Inline;\n    }\n\n    /**\n     * addDdElement\n     */\n    protected function addDdElement(array $Line, array $Block)\n    {\n        $text = substr($Line['text'], 1);\n        $text = trim($text);\n\n        unset($Block['dd']);\n\n        $Block['dd'] = array(\n            'name' => 'dd',\n            'handler' => array(\n                'function' => 'lineElements',\n                'argument' => $text,\n                'destination' => 'elements'\n            ),\n        );\n\n        if (isset($Block['interrupted']))\n        {\n            $Block['dd']['handler']['function'] = 'textElements';\n\n            unset($Block['interrupted']);\n        }\n\n        $Block['element']['elements'] []= & $Block['dd'];\n\n        return $Block;\n    }\n\n    /**\n     * buildFootnoteElement\n     */\n    protected function buildFootnoteElement()\n    {\n        $Element = array(\n            'name' => 'div',\n            'attributes' => array('class' => 'footnotes'),\n            'elements' => array(\n                array('name' => 'hr'),\n                array(\n                    'name' => 'ol',\n                    'elements' => [],\n                ),\n            ),\n        );\n\n        uasort($this->DefinitionData['Footnote'], 'self::sortFootnotes');\n\n        foreach ($this->DefinitionData['Footnote'] as $definitionId => $DefinitionData) {\n            if (!isset($DefinitionData['number'])) {\n                continue;\n            }\n\n            $text = $DefinitionData['text'];\n\n            $textElements = parent::textElements($text);\n\n            $numbers = range(1, $DefinitionData['count']);\n\n            $backLinkElements = [];\n\n            foreach ($numbers as $number) {\n                $backLinkElements[] = array('text' => ' ');\n                $backLinkElements[] = array(\n                    'name' => 'a',\n                    'attributes' => array(\n                        'href' => \"#fnref$number:$definitionId\",\n                        'rev' => 'footnote',\n                        'class' => 'footnote-backref',\n                    ),\n                    'rawHtml' => '&#8617;',\n                    'allowRawHtmlInSafeMode' => true,\n                    'autobreak' => false,\n                );\n            }\n\n            unset($backLinkElements[0]);\n\n            $n = count($textElements) -1;\n\n            if ($textElements[$n]['name'] === 'p') {\n                $backLinkElements = array_merge(\n                    array(\n                        array(\n                            'rawHtml' => '&#160;',\n                            'allowRawHtmlInSafeMode' => true,\n                        ),\n                    ),\n                    $backLinkElements\n                );\n\n                unset($textElements[$n]['name']);\n\n                $textElements[$n] = array(\n                    'name' => 'p',\n                    'elements' => array_merge(\n                        array($textElements[$n]),\n                        $backLinkElements\n                    ),\n                );\n            }\n            else {\n                $textElements[] = array(\n                    'name' => 'p',\n                    'elements' => $backLinkElements\n                );\n            }\n\n            $Element['elements'][1]['elements'] []= array(\n                'name' => 'li',\n                'attributes' => array('id' => 'fn:'.$definitionId),\n                'elements' => array_merge(\n                    $textElements\n                ),\n            );\n        }\n\n        return $Element;\n    }\n\n    /**\n     * parseAttributeData\n     */\n    protected function parseAttributeData($attributeString)\n    {\n        $Data = [];\n\n        $attributes = preg_split('/[ ]+/', $attributeString, - 1, PREG_SPLIT_NO_EMPTY);\n\n        foreach ($attributes as $attribute) {\n            if ($attribute[0] === '#') {\n                $Data['id'] = substr($attribute, 1);\n            }\n            // .\n            else {\n                $classes []= substr($attribute, 1);\n            }\n        }\n\n        if (isset($classes)) {\n            $Data['class'] = implode(' ', $classes);\n        }\n\n        return $Data;\n    }\n\n    /**\n     * processTag is recursive\n     */\n    protected function processTag($elementMarkup)\n    {\n        libxml_use_internal_errors(true);\n\n        // Steer away from using fragments since they mess up encoding\n        $elementMarkup = '<!DOCTYPE html><html><head><meta charset=\"UTF-8\"></head><body>' . $elementMarkup . '</body></html>';\n\n        // Load it up\n        $DOMDocument = new DOMDocument;\n        $DOMDocument->encoding = 'UTF-8';\n        $DOMDocument->loadHTML($elementMarkup, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);\n\n        // Something went wrong (missing body node)\n        $bodyNode = $DOMDocument->getElementsByTagName('body')->item(0);\n        if (!$bodyNode || !$bodyNode->firstChild) {\n            return $elementMarkup;\n        }\n\n        // Parse the markdown\n        $elementText = '';\n        if (\n            $bodyNode->firstChild->nodeType === XML_ELEMENT_NODE &&\n            $bodyNode->firstChild->getAttribute('markdown') === '1'\n        ) {\n            foreach ($bodyNode->firstChild->childNodes as $node) {\n                $elementText .= $DOMDocument->saveHTML($node);\n            }\n\n            $bodyNode->firstChild->removeAttribute('markdown');\n            $elementText = \"\\n\".$this->text($elementText).\"\\n\";\n        }\n        else {\n            foreach ($bodyNode->firstChild->childNodes as $node) {\n                $nodeMarkup = $DOMDocument->saveHTML($node);\n\n                if (\n                    $node instanceof DOMElement &&\n                    !in_array($node->nodeName, $this->textLevelElements)\n                ) {\n                    $elementText .= $this->processTag($nodeMarkup);\n                }\n                else {\n                    $elementText .= $nodeMarkup;\n                }\n            }\n        }\n\n        // Replacement to avoid markup getting encoded\n        $bodyNode->firstChild->nodeValue = 'placeholder\\x1A';\n\n        $markup = $DOMDocument->saveHTML($bodyNode->firstChild);\n        $markup = str_replace('placeholder\\x1A', $elementText, $markup);\n\n        return $markup;\n    }\n\n    /**\n     * sortFootnotes\n     */\n    protected function sortFootnotes($A, $B)\n    {\n        return $A['number'] - $B['number'];\n    }\n}\n"
  },
  {
    "path": "src/Parse/Syntax/FieldParser.php",
    "content": "<?php namespace October\\Rain\\Parse\\Syntax;\n\nuse Exception;\nuse Illuminate\\Support\\Arr;\n\n/**\n * FieldParser for dynamic syntax parser\n *\n * @package october\\parse\n * @author Alexey Bobkov, Samuel Georges\n */\nclass FieldParser\n{\n    /**\n     * @var string template contents\n     */\n    protected $template = '';\n\n    /**\n     * @var array Extracted fields from the template\n     * The array key should match a unique field name, and the value\n     * is another array with values:\n     *\n     * - type: the tag name, eg: text\n     * - default: the default tag text\n     * - *: defined parameters\n     */\n    protected $fields = [];\n\n    /**\n     * @var array Complete tag strings for each field. The array\n     * key will match the unique field name and the value is the\n     * complete tag string, eg: {text}...{/text}\n     */\n    protected $tags = [];\n\n    /**\n     * @var string A prefix to place before all tag references\n     * eg: {namespace:text}{/namespace:text}\n     */\n    protected $tagPrefix = '';\n\n    /**\n     * @var array Registered template tags\n     */\n    protected $registeredTags = [\n        'text',\n        'textarea',\n        'richeditor',\n        'markdown',\n        'fileupload',\n        'mediafinder',\n        'dropdown',\n        'colorpicker',\n        'radio',\n        'checkbox',\n        'checkboxlist',\n        'datepicker',\n        'balloon-selector',\n        'repeater',\n        'variable'\n    ];\n\n    /**\n     * Constructor\n     * @param string $template Template to parse.\n     */\n    public function __construct($template = null, $options = [])\n    {\n        if ($template) {\n            $this->tagPrefix = Arr::get($options, 'tagPrefix', '');\n            $this->template = $template;\n            $this->processTemplate($template);\n        }\n    }\n\n    /**\n     * Processes repeating tags first, then registered tags and assigns\n     * the results to local object properties.\n     * @return void\n     */\n    protected function processTemplate($template)\n    {\n        // Process repeaters\n        list($template, $repeatTags, $repeatfields) = $this->processRepeaterTags($template);\n\n        // Process registered tags\n        list($tags, $fields) = $this->processTags($template);\n        $this->tags += $tags;\n        $this->fields += $fields;\n\n        /*\n         * Layer the repeater tags over the standard ones to retain\n         * the original sort order\n         */\n        foreach ($repeatfields as $field => $params) {\n            $this->fields[$field] = $params;\n        }\n\n        foreach ($repeatTags as $field => $params) {\n            $this->tags[$field] = $params;\n        }\n    }\n\n    /**\n     * Static helper for new instances of this class.\n     * @param string $template\n     * @param array $options\n     * @return FieldParser\n     */\n    public static function parse($template, $options = [])\n    {\n        return new static($template, $options);\n    }\n\n    /**\n     * Returns all tag strings found in the template\n     * @return array\n     */\n    public function getTags()\n    {\n        return $this->tags;\n    }\n\n    /**\n     * Returns tag strings for a specific field\n     * @param  string $field\n     * @return array\n     */\n    public function getFieldTags($field)\n    {\n        return $this->tags[$field] ?? [];\n    }\n\n    /**\n     * Returns all field definitions found in the template\n     * @return array\n     */\n    public function getFields()\n    {\n        return $this->fields;\n    }\n\n    /**\n     * Returns defined parameters for a single field\n     * @param  string $field\n     * @return array\n     */\n    public function getFieldParams($field)\n    {\n        return $this->fields[$field] ?? [];\n    }\n\n    /**\n     * Returns default values for all fields.\n     * @param  array $fields\n     * @return array\n     */\n    public function getDefaultParams($fields = null)\n    {\n        if (is_null($fields)) {\n            $fields = $this->fields;\n        }\n\n        $defaults = [];\n\n        foreach ($fields as $field => $params) {\n            if (!isset($params['type'])) {\n                continue;\n            }\n\n            if ($params['type'] === 'repeater') {\n                $defaults[$field] = [];\n                $defaults[$field][] = $this->getDefaultParams(Arr::get($params, 'fields', []));\n            }\n            else {\n                $defaults[$field] = $params['default'] ?? null;\n            }\n        }\n\n        return $defaults;\n    }\n\n    /**\n     * Processes all repeating tags against a template, this will strip\n     * any repeaters from the template for further processing.\n     * @param  string $template\n     * @return void\n     */\n    protected function processRepeaterTags($template)\n    {\n        list($tags, $fields) = $this->processTags($template, ['repeater']);\n\n        foreach ($fields as $name => &$field) {\n            $outerTemplate = $tags[$name];\n            $innerTemplate = $field['default'];\n            unset($field['default']);\n            list($innerTags, $innerFields) = $this->processTags($innerTemplate);\n            list($openTag, $closeTag) = explode($innerTemplate, $outerTemplate);\n\n            $field['fields'] = $innerFields;\n            $tags[$name] = [\n                'tags' => $innerTags,\n                'template' => $outerTemplate,\n                'open' => $openTag,\n                'close' => $closeTag\n            ];\n\n            // Remove the inner content of the repeater\n            // tag to prevent further parsing\n            $template = str_replace($outerTemplate, $openTag.$closeTag, $template);\n        }\n\n        return [$template, $tags, $fields];\n    }\n\n    /**\n     * Processes all registered tags against a template.\n     * @param  string $template\n     * @param  bool $usingTags\n     * @return void\n     */\n    protected function processTags($template, $usingTags = null)\n    {\n        if (!$usingTags) {\n            $usingTags = $this->registeredTags;\n        }\n\n        if ($this->tagPrefix) {\n            foreach ($usingTags as $tag) {\n                $usingTags[] = $this->tagPrefix . $tag;\n            }\n        }\n\n        $tags = [];\n        $fields = [];\n\n        $result = $this->processTagsRegex($template, $usingTags);\n        $tagStrings = $result[0];\n        $tagNames = $result[1];\n        $paramStrings = $result[2];\n\n        foreach ($tagStrings as $key => $tagString) {\n            $tagName = $tagNames[$key];\n            $params = $this->processParams($paramStrings[$key], $tagName);\n\n            if (isset($params['name'])) {\n                $name = $params['name'];\n                unset($params['name']);\n            }\n            else {\n                $name = md5($tagString);\n            }\n\n            if ($tagName === 'variable') {\n                $params['X_OCTOBER_IS_VARIABLE'] = true;\n                $tagName = Arr::get($params, 'type', 'text');\n            }\n\n            $params['type'] = $tagName;\n\n            // Convert known properties to array\n            $arrayProps = ['trigger', 'options', 'availableColors'];\n            foreach ($arrayProps as $prop) {\n                if (!isset($params[$prop])) {\n                    continue;\n                }\n\n                $params[$prop] = $this->processOptionsToArray($params[$prop]);\n            }\n\n            $tags[$name] = $tagString;\n            $fields[$name] = $params;\n        }\n\n        return [$tags, $fields];\n    }\n\n    /**\n     * Processes group 2 from the Tag regex and returns\n     * an array of captured parameters.\n     * @param  string $value\n     * @param  string $tagName\n     * @return array\n     */\n    protected function processParams($value, $tagName)\n    {\n        $close = Parser::CHAR_CLOSE;\n        $closePos = strpos($value, $close);\n        $defaultValue = '';\n        if ($closePos === false) {\n            $paramString = $value;\n        }\n        elseif (substr($value, -1) === $close) {\n            $paramString = substr($value, 0, -1);\n        }\n        else {\n            $paramString = substr($value, 0, $closePos);\n            $defaultValue = trim(substr($value, $closePos + 1));\n        }\n\n        $result = $this->processParamsRegex($paramString);\n        $paramNames = $result[1];\n        $paramValues = $result[2];\n\n        // Convert all 'true' and 'false' string values to boolean values\n        foreach ($paramValues as $key => $value) {\n            if ($value === 'true' || $value === 'false') {\n                $paramValues[$key] = $value === 'true';\n            }\n        }\n\n        $params = array_combine($paramNames, $paramValues);\n\n        if ($tagName === 'checkbox') {\n            $params['_content'] = $defaultValue;\n        }\n        else {\n            $params['default'] = $defaultValue;\n        }\n\n        return $params;\n    }\n\n    /**\n     * processParamsRegex converts parameter string to an array.\n     *\n     *  In: name=\"test\" comment=\"This is a test\"\n     *  Out: ['name' => 'test', 'comment' => 'This is a test']\n     *\n     * @param  string $string\n     * @return array\n     */\n    protected function processParamsRegex($string)\n    {\n        /*\n         * Match key/value pairs\n         *\n         * (\\w+)=\"((?:\\\\.|[^\"\\\\]+)*|[^\"]*)\"\n         */\n        $regex = '#';\n        $regex .= '(\\w+)'; // Any word\n        $regex .= '=\"'; // Equal sign and open quote\n\n        $regex .= '('; // Capture\n        $regex .= '(?:\\\\\\\\.|[^\"\\\\\\\\]+)*'; // Include escaped quotes \\\"\n        $regex .= '|[^\"]'; // Or anything other than a quote\n        $regex .= '*)'; // Capture value\n        $regex .= '\"';\n        $regex .= '#';\n\n        preg_match_all($regex, $string, $match);\n\n        return $match;\n    }\n\n    /**\n     * Performs a regex looking for a field type (key) and returns\n     * an array where:\n     *\n     *  0 - The full tag definition, eg: {text name=\"test\"}Foobar{/text}\n     *  1 - The opening and closing tag name\n     *  2 - The tag parameters as a string, eg: name=\"test\"} and;\n     *  2 - The default text inside the tag (optional), eg: Foobar\n     *\n     * @param  string $string\n     * @param  string $tags\n     * @return array\n     */\n    protected function processTagsRegex($string, $tags)\n    {\n        /*\n         * Match opening and close tags\n         *\n         * {(text|textarea)\\s([\\S\\s]+?){/(?:\\1)}\n         */\n        $open = preg_quote(Parser::CHAR_OPEN);\n        $close = preg_quote(Parser::CHAR_CLOSE);\n        $tags = implode('|', $tags);\n\n        $regex = '#';\n        $regex .= $open.'('.$tags.')\\s'; // Group 1\n        $regex .= '([\\S\\s]+?)'; // Group 2 (Non greedy)\n        $regex .= $open.'/(?:\\1)'.$close; // Group X (Not captured)\n        $regex .= '#';\n\n        preg_match_all($regex, $string, $match);\n\n        return $match;\n    }\n\n    /**\n     * Splits an option string to an array.\n     *\n     * one|two           -> [one, two]\n     * one:One|two:Two   -> [one => 'One', two => 'Two']\n     * ClassName::method -> [...]\n     *\n     * @param  string $optionsString\n     * @return array\n     */\n    protected function processOptionsToArray($optionsString)\n    {\n        $result = [];\n\n        if (strpos($optionsString, '::') !== false) {\n            $options = explode('::', $optionsString);\n            if (\n                count($options) === 2 &&\n                class_exists($options[0]) &&\n                method_exists($options[0], $options[1])\n            ) {\n                $result = $options[0]::{$options[1]}();\n                if (!is_array($result)) {\n                    throw new Exception(sprintf(\n                        'Invalid dropdown option array returned by `%s::%s`',\n                        $options[0],\n                        $options[1]\n                    ));\n                }\n\n                return $result;\n            }\n        }\n\n        $options = explode('|', $optionsString);\n\n        foreach ($options as $index => $optionStr) {\n            $parts = explode(':', $optionStr, 2);\n\n            if (count($parts) > 1) {\n                $key = trim($parts[0]);\n\n                if (strlen($key)) {\n                    if (!preg_match('/^[0-9a-z-_]+$/i', $key)) {\n                        throw new Exception(sprintf(\n                            'Invalid drop-down option key: %s. Option keys can contain only digits, Latin letters and characters _ and -',\n                            $key\n                        ));\n                    }\n\n                    $result[$key] = trim($parts[1]);\n                }\n                else {\n                    $result[$index] = trim($optionStr);\n                }\n            }\n            else {\n                $result[$index] = trim($optionStr);\n            }\n        }\n\n        return $result;\n    }\n}\n"
  },
  {
    "path": "src/Parse/Syntax/Parser.php",
    "content": "<?php namespace October\\Rain\\Parse\\Syntax;\n\nuse Illuminate\\Support\\Arr;\nuse October\\Rain\\Parse\\Bracket as TextParser;\n\n/**\n * Parser for Dynamic Syntax\n *\n * @package october\\parse\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Parser\n{\n    const CHAR_OPEN = '{';\n    const CHAR_CLOSE = '}';\n\n    /**\n     * @var string template to parse\n     */\n    protected $template;\n\n    /**\n     * @var \\October\\Rain\\Parse\\Syntax\\FieldParser Field parser instance.\n     */\n    protected $fieldParser;\n\n    /**\n     * @var \\October\\Rain\\Parse\\Bracket Text parser instance.\n     */\n    protected $textParser;\n\n    /**\n     * @var string A prefix to place before all variable references\n     * when rendering the view.\n     */\n    protected $varPrefix = '';\n\n    /**\n     * __construct parser, with available options:\n     *   - varPrefix: Prefix to add to every top level parameter.\n     *   - tagPrefix: Prefix to add to all tags, in addition to tags without a prefix.\n     * @param string $template\n     * @param array $options\n     */\n    public function __construct($template = null, $options = [])\n    {\n        if ($template) {\n            $this->template = $template;\n            $this->varPrefix = Arr::get($options, 'varPrefix', '');\n            $this->fieldParser = new FieldParser($template, $options);\n\n            $textFilters = [\n                'md' => ['Markdown', 'parse'],\n                'media' => [\\Media\\Classes\\MediaLibrary::class, 'url'],\n            ];\n\n            $this->textParser = new TextParser(['filters' => $textFilters]);\n        }\n    }\n\n    /**\n     * Static helper for new instances of this class.\n     * @param  string $template\n     * @param  array $options\n     * @return self\n     */\n    public static function parse($template, $options = [])\n    {\n        return new static($template, $options);\n    }\n\n    /**\n     * Renders the template fields to their actual values\n     * @param  array $vars\n     * @param  array $options\n     * @return string\n     */\n    public function render($vars = [], $options = [])\n    {\n        $vars = array_replace_recursive($this->getFieldValues(), (array) $vars);\n        $this->textParser->setOptions($options);\n        return $this->textParser->parseString($this->toView(), $vars);\n    }\n\n    /**\n     * Returns the default field values defined in the template\n     * @return array\n     */\n    public function getFieldValues()\n    {\n        return $this->fieldParser->getDefaultParams();\n    }\n\n    /**\n     * Returns an array of all fields and their options.\n     * @return array\n     */\n    public function toEditor()\n    {\n        return $this->fieldParser->getFields();\n    }\n\n    /**\n     * Returns the template with fields replaced with Twig markup\n     * @return string\n     */\n    public function toTwig()\n    {\n        return $this->toViewEngine('twig');\n    }\n\n    /**\n     * toView returns the template with fields replaced with the simple\n     * template engine used by the TextParser class.\n     * @return string\n     */\n    public function toView()\n    {\n        return $this->toViewEngine('simple');\n    }\n\n    /**\n     * toViewEngine parses the template to a specific view engine (Twig, Simple)\n     * @param  string $engine\n     * @return string\n     */\n    protected function toViewEngine($engine)\n    {\n        $engine = ucfirst($engine);\n        $template = $this->template;\n\n        $tags = $this->fieldParser->getTags();\n        foreach ($tags as $field => $tag) {\n            $template = is_array($tag)\n                ? $this->processRepeatingTag($engine, $template, $field, $tag)\n                : $this->processTag($engine, $template, $field, $tag);\n        }\n\n        return $template;\n    }\n\n    /**\n     * processRepeatingTag\n     */\n    protected function processRepeatingTag($engine, $template, $field, $tagDetails)\n    {\n        $prefixField = $this->varPrefix.$field;\n        $params = $this->fieldParser->getFieldParams($field);\n        $innerFields = Arr::get($params, 'fields', []);\n        $innerTags = $tagDetails['tags'];\n        $innerTemplate = $tagDetails['template'];\n\n        /*\n         * Replace all the inner tags\n         */\n        foreach ($innerTags as $innerField => $tagString) {\n            $innerParams = Arr::get($innerFields, $innerField, []);\n            $tagReplacement = $this->{'eval'.$engine.'ViewField'}($innerField, $innerParams, 'fields');\n            $innerTemplate = str_replace($tagString, $tagReplacement, $innerTemplate);\n        }\n\n        /*\n         * Replace the opening tag\n         */\n        $openTag = Arr::get($tagDetails, 'open', '{repeater}');\n        $openReplacement = $engine === 'Twig' ? '{% for fields in '.$prefixField.' %}' : '{'.$prefixField.'}';\n        $openReplacement = $openReplacement . PHP_EOL;\n        $innerTemplate = str_replace($openTag, $openReplacement, $innerTemplate);\n\n        /*\n         * Replace the closing tag\n         */\n        $closeTag = Arr::get($tagDetails, 'close', '{/repeater}');\n        $closeReplacement = $engine === 'Twig' ? '{% endfor %}' : '{/'.$prefixField.'}';\n        $closeReplacement = PHP_EOL . $closeReplacement;\n        $innerTemplate = str_replace($closeTag, $closeReplacement, $innerTemplate);\n\n        $templateString = $tagDetails['template'];\n        $template = str_replace($templateString, $innerTemplate, $template);\n        return $template;\n    }\n\n    /**\n     * processTag\n     */\n    protected function processTag($engine, $template, $field, $tagString)\n    {\n        $prefixField = $this->varPrefix.$field;\n        $params = $this->fieldParser->getFieldParams($field);\n        $tagReplacement = $this->{'eval'.$engine.'ViewField'}($prefixField, $params);\n        $template = str_replace($tagString, $tagReplacement, $template);\n        return $template;\n    }\n\n    /**\n     * evalTwigViewField processes a field type and converts it to the Twig engine.\n     * @param  string $field\n     * @param  array $params\n     * @param  string $prefix\n     * @return string\n     */\n    protected function evalTwigViewField($field, $params, $prefix = null)\n    {\n        if (isset($params['X_OCTOBER_IS_VARIABLE'])) {\n            return '';\n        }\n\n        // Used by Twig for loop\n        if ($prefix) {\n            $field = $prefix.'.'.$field;\n        }\n\n        $type = $params['type'] ?? 'text';\n\n        switch ($type) {\n            default:\n            case 'text':\n            case 'textarea':\n                $result = '{{ ' . $field . ' }}';\n                break;\n            case 'markdown':\n                $result = '{{ ' . $field . '|md }}';\n                break;\n            case 'richeditor':\n                $result = '{{ ' . $field . '|raw }}';\n                break;\n            case 'mediafinder':\n                $result = '{{ ' . $field . '|media }}';\n                break;\n            case 'checkbox':\n                $result = '{% if ' . $field . ' %}' . $params['_content'] . '{% endif %}';\n                break;\n            case 'datepicker':\n                switch ($params['mode']) {\n                    default:\n                    case 'datetime':\n                        $result = '{{ ' . $field . '|date(\"Y-m-d H:i:s\") }}';\n                        break;\n                    case 'date':\n                        $result = '{{ ' . $field . '|date(\"Y-m-d\") }}';\n                        break;\n                    case 'time':\n                        $result = '{{ ' . $field . '|date(\"H:i:s\") }}';\n                        break;\n                }\n                break;\n        }\n\n        return $result;\n    }\n\n    /**\n     * Processes a field type and converts it to the Simple engine.\n     * @param  string $field\n     * @param  array $params\n     * @return string\n     */\n    protected function evalSimpleViewField($field, $params, $prefix = null)\n    {\n        if (isset($params['X_OCTOBER_IS_VARIABLE'])) {\n            return '';\n        }\n\n        $type = $params['type'] ?? 'text';\n\n        switch ($type) {\n            case 'markdown':\n                $result = static::CHAR_OPEN . $field . '|md' . static::CHAR_CLOSE;\n                break;\n            case 'mediafinder':\n                $result = static::CHAR_OPEN . $field . '|media' . static::CHAR_CLOSE;\n                break;\n            case 'checkbox':\n                $result = static::CHAR_OPEN . '?' . $field . static::CHAR_CLOSE;\n                $result .= $params['_content'];\n                $result .= static::CHAR_OPEN . '/' . $field . static::CHAR_CLOSE;\n                break;\n            default:\n                $result = static::CHAR_OPEN . $field . static::CHAR_CLOSE;\n                break;\n        }\n\n        return $result;\n    }\n}\n"
  },
  {
    "path": "src/Parse/Syntax/README.md",
    "content": "# Rain Dynamic Syntax\n\nDynamic Syntax is a templating engine that supports two modes of rendering. Parsing template text can produce two results, either a **view** or **editor** mode. Using this template text as an example:\n\n    <h1>{text name=\"websiteName\" label=\"Website Name\"}Our wonderful website{/text}</h1>\n\nThe inner part of the `{text}...{/text}` tags represents the default **view** text, the remaining properties (name and label) are used primarily for the **editor** mode.\n\n## Class usage\n\nCalling `$syntax->render($params)` will render the template:\n\n    <h1>Our wonderful website</h1>\n\nCalling `$syntax->toTwig()` will render as Twig markup:\n\n    <h1>{{ websiteName }}</h1>\n\nCalling `$syntax->toEditor()` will return an array:\n\n    'websiteName' => [\n        'label' => 'Website name',\n        'default' => 'Our wonderful website',\n        'type' => 'text'\n    ]\n\nExample\n\n    $syntax = Parser::parse('<h1>{text name=\"websiteName\" label=\"Website Name\"}Our wonderful website{/text}</h1>');\n\n    // Outputs <h1>{{ websiteName }}</h1>\n    echo $syntax->toView();\n\n    // Returns ['websiteName' => [...] ]\n    $syntax->toEditor();\n\n    // Outputs <h1>Our wonderful website</h1>\n    echo $syntax->render();\n\n    // Outputs <h1>Your awesome web page</h1>\n    echo $syntax->render(['websiteName' => 'Your awesome web page']);\n\n## Supported tags\n\n### Text\n\nRenders a single line editor field for smaller blocks of text. The view value is the text entered.\n\n    {text name=\"websiteName\" label=\"Website Name\"}Our wonderful website{/text}\n\n### Textarea\n\nRenders a multiple line editor field for larger blocks of text. The view value is the text entered.\n\n    {textarea name=\"websiteDescription\" label=\"Website Description\"}This is our vision for things to come{/textarea}\n\n### Dropdown\n\nRenders a dropdown form field.\n\n    {dropdown name=\"dropdown\" label=\"Pick one\" options=\"One|Two\"}{/dropdown}\n\n### Radio\n\nRenders a radio form field.\n\n    {radio name=\"radio\" label=\"Thoughts?\" options=\"y:Yes|n:No|m:Maybe\"}{/radio}\n\n### Rich editor\n\nRenders a WYSIWYG content editor.\n\n    {richeditor name=\"content\" label=\"Main content\"}Default text{/richeditor}\n\nRenders in Twig as\n\n    {{ content|raw }}\n\n### Markdown\n\nRenders a Markdown content editor.\n\n    {markdown name=\"content\" label=\"Markdown content\"}Default text{/markdown}\n\nRenders in Twig as\n\n    {{ content|md }}\n\n### Checkbox\n\nRenders conditional content inside (still under development)\n\n    {checkbox name=\"showHeader\" label=\"Show heading\" default=\"true\"}\n        <p>This content will be shown if the checkbox is ticked</p>\n    {/checkbox}\n\nRenders in Twig as\n\n    {% if checkbox %}\n        {{ showHeader }}\n    {% endif %}\n\n### File Upload\n\nRenders a file upload editor field. The view value is the full path to the file.\n\n    {fileupload name=\"logo\" label=\"Logo\"}defaultlogo.png{/fileupload}\n\n### Repeater\n\nRenders a repeating section with other fields inside.\n\n    {repeater name=\"content_sections\" prompt=\"Add another content section\"}\n        <h2>{text name=\"title\" label=\"Title\"}Title{/text}</h2>\n        <p>{textarea name=\"content\" label=\"Content\"}Content{/textarea}</p>\n    {/repeater}\n\nRenders in Twig as\n\n    {% for fields in repeater %}\n        <h2>{{ fields.title }}</h2>\n        <p>{{ fields.content|raw }}</p>\n    {% endfor %}\n\nCalling `$syntax->toEditor()` will return a different array for a repeater field:\n\n    'repeater' => [\n        'label' => 'Website name',\n        'type' => 'repeater'\n        'fields' => [\n\n            'title' => [\n                'label' => 'Title',\n                'default' => 'Title',\n                'type' => 'text'\n            ],\n            'content' => [\n                'label' => 'Content',\n                'default' => 'Content',\n                'type' => 'textarea'\n            ]\n\n        ]\n    ]\n\n### Variable\n\nUsed for adding fields to editor mode only. This tag will not affect the view mode and will be replaced with an empty string.\n\n    {variable type=\"text\" name=\"websiteName\" label=\"Website Name\"}Our wonderful website{/variable}\n"
  },
  {
    "path": "src/Parse/Syntax/SyntaxModelTrait.php",
    "content": "<?php namespace October\\Rain\\Parse\\Syntax;\n\nuse Illuminate\\Support\\Arr;\nuse October\\Rain\\Support\\Str;\nuse Request;\n\n/**\n * SyntaxModelTrait for use in models\n *\n * @package october\\parse\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait SyntaxModelTrait\n{\n    /**\n     * @deprecated replace with initializeSyntaxModelTrait model.afterFetch\n     */\n    public static function bootSyntaxModelTrait()\n    {\n        static::fetched(function ($model) {\n            $model->defineSyntaxRelations();\n        });\n    }\n\n    /**\n     * initializeSyntaxModelTrait constructor\n     */\n    public function initializeSyntaxModelTrait()\n    {\n        $this->bindEvent('model.beforeReplicate', function() {\n            $this->defineSyntaxRelations();\n        });\n    }\n\n    /**\n     * defineSyntaxRelations defines any relationships (attachments) that this model\n     * will need based on the field definitions.\n     */\n    public function defineSyntaxRelations()\n    {\n        $fields = $this->getSyntaxFields();\n        if (!is_array($fields)) {\n            return;\n        }\n\n        foreach ($fields as $field => $params) {\n            if (!isset($params['type'])) {\n                continue;\n            }\n\n            if ($params['type'] === 'fileupload') {\n                $this->attachOne[$field] = \\System\\Models\\File::class;\n            }\n        }\n    }\n\n    /**\n     * getFormSyntaxData prepares the syntax field data for saving.\n     */\n    public function getFormSyntaxData()\n    {\n        $data = $this->getSyntaxData();\n\n        $fields = $this->getSyntaxFields();\n        if (!is_array($fields)) {\n            return $data;\n        }\n\n        foreach ($fields as $field => $params) {\n            if (!isset($params['type'])) {\n                continue;\n            }\n\n            // File upload\n            if ($params['type'] === 'fileupload' && $this->hasRelation($field)) {\n                if ($this->sessionKey) {\n                    if ($image = $this->$field()->withDeferred($this->sessionKey)->first()) {\n                        $data[$field] = $this->getThumbForImage($image, $params);\n                    }\n                    else {\n                        unset($data[$field]);\n                    }\n                }\n                elseif ($this->$field) {\n                    $data[$field] = $this->getThumbForImage($this->$field, $params);\n                }\n            }\n        }\n\n        return $data;\n    }\n\n    /**\n     * getThumbForImage helper to get the perfect sized image.\n     */\n    protected function getThumbForImage($image, $params = [])\n    {\n        $imageWidth = Arr::get($params, 'imageWidth');\n        $imageHeight = Arr::get($params, 'imageHeight');\n        if ($imageWidth && $imageHeight) {\n            $path = $image->getThumb($imageWidth, $imageHeight, ['mode' => 'crop']);\n        }\n        else {\n            $path = $image->getPath();\n        }\n\n        if (!Str::startsWith($path, ['//', 'http://', 'https://'])) {\n            $path = Request::getSchemeAndHttpHost() . $path;\n        }\n\n        return $path;\n    }\n\n    /**\n     * getFormSyntaxFields prepares the syntax fields for use in a Form builder.\n     * The array name is added to each field.\n     * @return array\n     */\n    public function getFormSyntaxFields()\n    {\n        $fields = $this->getSyntaxFields();\n        if (!is_array($fields)) {\n            return [];\n        }\n\n        $newFields = [];\n        foreach ($fields as $field => $params) {\n            if (!isset($params['type'])) {\n                continue;\n            }\n\n            if ($params['type'] !== 'fileupload') {\n                $newField = $this->getSyntaxDataColumnName().'['.$field.']';\n            }\n            else {\n                $newField = $field;\n            }\n\n            if ($params['type'] === 'repeater') {\n                $params['form']['fields'] = Arr::get($params, 'fields', []);\n                unset($params['fields']);\n            }\n\n            $newFields[$newField] = $params;\n        }\n\n        return $newFields;\n    }\n\n    /**\n     * makeSyntaxFields processes supplied content and extracts the field definitions\n     * and default data. It is mixed with the current data and applied\n     * to the fields and data attributes.\n     * @param string $content\n     * @return array\n     */\n    public function makeSyntaxFields($content)\n    {\n        $parser = Parser::parse($content);\n        $fields = $parser->toEditor() ?: [];\n\n        $this->setAttribute($this->getSyntaxFieldsColumnName(), $fields);\n\n        // Remove fields no longer present and add default values\n        $currentFields = array_intersect_key((array) $this->getFormSyntaxData(), $parser->getFieldValues());\n        $currentFields = $currentFields + $parser->getFieldValues();\n\n        $this->setAttribute($this->getSyntaxDataColumnName(), $currentFields);\n\n        return $fields;\n    }\n\n    /**\n     * getSyntaxParser\n     */\n    public function getSyntaxParser($content)\n    {\n        return Parser::parse($content);\n    }\n\n    /**\n     * getSyntaxDataColumnName returns the data column name.\n     * @return string\n     */\n    public function getSyntaxDataColumnName()\n    {\n        return defined('static::SYNTAX_DATA') ? static::SYNTAX_DATA : 'syntax_data';\n    }\n\n    /**\n     * getSyntaxData returns value of the model syntax_data column.\n     * @return int\n     */\n    public function getSyntaxData()\n    {\n        return $this->getAttribute($this->getSyntaxDataColumnName());\n    }\n\n    /**\n     * getSyntaxFieldsColumnName returns fields column name.\n     * @return string\n     */\n    public function getSyntaxFieldsColumnName()\n    {\n        return defined('static::SYNTAX_FIELDS') ? static::SYNTAX_FIELDS : 'syntax_fields';\n    }\n\n    /**\n     * getSyntaxFields returns value of the model syntax_fields column.\n     * @return int\n     */\n    public function getSyntaxFields()\n    {\n        return $this->getAttribute($this->getSyntaxFieldsColumnName());\n    }\n}\n"
  },
  {
    "path": "src/Parse/Twig.php",
    "content": "<?php namespace October\\Rain\\Parse;\n\nuse App;\n\n/**\n * Twig helper class\n *\n * @package october\\parse\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Twig\n{\n    /**\n     * parse supplied Twig contents, with supplied variables.\n     * @param string $contents Twig contents to parse.\n     * @param array $vars Context variables.\n     * @return string\n     */\n    public function parse($contents, $vars = [])\n    {\n        $twig = App::make('twig.environment');\n        $template = $twig->createTemplate($contents);\n        return $template->render($vars);\n    }\n}\n"
  },
  {
    "path": "src/Parse/Yaml.php",
    "content": "<?php namespace October\\Rain\\Parse;\n\nuse Cache;\nuse Symfony\\Component\\Yaml\\Yaml as YamlComponent;\nuse Symfony\\Component\\Yaml\\Dumper;\nuse Symfony\\Component\\Yaml\\Parser;\nuse Symfony\\Component\\Yaml\\Exception\\ParseException;\nuse Exception;\n\n/**\n * Yaml helper class\n *\n * @package october\\parse\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Yaml\n{\n    /**\n     * parse supplied YAML contents in to a PHP array.\n     * @param string $contents YAML contents to parse.\n     * @return array The YAML contents as an array.\n     */\n    public function parse($contents)\n    {\n        $yaml = new Parser;\n\n        return $yaml->parse($contents);\n    }\n\n    /**\n     * parseFile parses YAML file contents in to a PHP array.\n     * @param string $fileName File to read contents and parse.\n     * @return array The YAML contents as an array.\n     */\n    public function parseFile($fileName)\n    {\n        $contents = file_get_contents($fileName);\n\n        try {\n            $parsed = $this->parse($contents);\n        }\n        catch (Exception $ex) {\n            throw new ParseException(\"A syntax error was detected in $fileName. \" . $ex->getMessage(), __LINE__, __FILE__);\n        }\n\n        return $parsed;\n    }\n\n    /**\n     * parseFileCached parses YAML file contents in to a PHP array, with cache.\n     * @param string $fileName File to read contents and parse.\n     * @return array The YAML contents as an array.\n     */\n    public function parseFileCached($fileName)\n    {\n        try {\n            $fileCacheKey = 'yaml::' . $fileName . '-' . filemtime($fileName);\n\n            return Cache::memo()->remember($fileCacheKey, 43200, function () use ($fileName) {\n                return $this->parseFile($fileName);\n            });\n        }\n        catch (Exception $ex) {\n            return $this->parseFile($fileName);\n        }\n    }\n\n    /**\n     * render a PHP array to YAML format.\n     *\n     * Supported options:\n     * - inline: The level where you switch to inline YAML.\n     * - exceptionOnInvalidType: if an exception must be thrown on invalid types.\n     * - objectSupport: if object support is enabled.\n     *\n     * @param array $vars\n     * @param array $options\n     * @return string\n     */\n    public function render($vars = [], $options = [])\n    {\n        extract(array_merge([\n            'inline' => 20,\n            'exceptionOnInvalidType' => false,\n            'objectSupport' => true,\n        ], $options));\n\n        $flags = null;\n\n        if ($exceptionOnInvalidType) {\n            $flags |= YamlComponent::DUMP_EXCEPTION_ON_INVALID_TYPE;\n        }\n\n        if ($objectSupport) {\n            $flags |= YamlComponent::DUMP_OBJECT;\n        }\n\n        return (new Dumper)->dump($vars, $inline, 0, $flags);\n    }\n}\n"
  },
  {
    "path": "src/Resize/ResizeBuilder.php",
    "content": "<?php namespace October\\Rain\\Resize;\n\n/**\n * ResizeBuilder builds resizers on demand\n *\n * @package october\\resize\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ResizeBuilder\n{\n    public function open($filename)\n    {\n        return new Resizer($filename);\n    }\n}\n"
  },
  {
    "path": "src/Resize/ResizeServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Resize;\n\nuse October\\Rain\\Support\\ServiceProvider;\nuse Illuminate\\Contracts\\Support\\DeferrableProvider;\n\n/**\n * ResizeServiceProvider\n *\n * @package october\\resize\n * @author Alexey Bobkov, Samuel Georges\n */\nclass ResizeServiceProvider extends ServiceProvider implements DeferrableProvider\n{\n    /**\n     * register the service provider.\n     */\n    public function register()\n    {\n        $this->app->singleton('resizer', function ($app) {\n            return new ResizeBuilder;\n        });\n    }\n\n    /**\n     * provides the returned services.\n     * @return array\n     */\n    public function provides()\n    {\n        return ['resizer'];\n    }\n}\n"
  },
  {
    "path": "src/Resize/Resizer.php",
    "content": "<?php namespace October\\Rain\\Resize;\n\nuse Intervention\\Image\\Interfaces\\ImageInterface;\nuse Symfony\\Component\\HttpFoundation\\File\\File as FileObj;\nuse Exception;\n\n/**\n * Resizer for images\n *\n * Available options are:\n *  - mode: Either exact, portrait, landscape, auto, fit, cover or crop.\n *  - offset: The offset of the crop = [ left, top ]\n *  - sharpen: Sharpen image, from 0 - 100 (default: 0)\n *  - interlace: Interlace image,  Boolean: false (disabled: default), true (enabled)\n *  - quality: Image quality, from 0 - 100 (default: 90)\n *\n * @package october\\resize\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Resizer\n{\n    /**\n     * @var FileObj file the symfony uploaded file object\n     */\n    protected $file;\n\n    /**\n     * @var string extension of the uploaded file\n     */\n    protected $extension;\n\n    /**\n     * @var string mime type of the uploaded file\n     */\n    protected $mime;\n\n    /**\n     * @var \\GdImage image (on disk) that's being resized\n     */\n    protected $image;\n\n    /**\n     * @var \\GdImage originalImage cached\n     */\n    protected $originalImage;\n\n    /**\n     * @var int width of the original image being resized\n     */\n    protected $width;\n\n    /**\n     * @var int height of the original image being resized\n     */\n    protected $height;\n\n    /**\n     * @var int|null orientation (Exif) of image\n     */\n    protected $orientation;\n\n    /**\n     * @var array options used for resizing\n     */\n    protected $options = [];\n\n\n    /**\n     * __construct instantiates the Resizer and receives the path to an image we're working with.\n     * The file can be either Input::file('field_name') or a path to a file\n     * @param mixed $file\n     */\n    public function __construct($file)\n    {\n        if (!$file) {\n            throw new Exception('Opened resizer on an empty file');\n        }\n\n        if (is_string($file)) {\n            $file = new FileObj($file);\n        }\n\n        $this->file = $file;\n\n        // Get the file extension\n        $this->extension = $file->guessExtension();\n        $this->mime = $file->getMimeType();\n\n        // Open up the file\n        $this->image = $this->openImage($file);\n\n        // Get width and height of our image\n        $this->width  = $this->image->width();\n        $this->height = $this->image->height();\n\n        // Set default options\n        $this->setOptions([]);\n    }\n\n    /**\n     * open is a static constructor\n     */\n    public static function open($file): Resizer\n    {\n        return new Resizer($file);\n    }\n\n    /**\n     * setOptions sets resizer options\n     */\n    public function setOptions(array $options): static\n    {\n        $this->options = array_merge([\n            'mode' => 'auto',\n            'offset' => [0, 0],\n            'sharpen' => 0,\n            'interlace' => false,\n            'quality' => 90\n        ], $options);\n\n        return $this;\n    }\n\n    /**\n     * getOption gets an individual resizer option\n     * @param string $option\n     */\n    protected function getOption($option)\n    {\n        return $this->options[$option] ?? null;\n    }\n\n    /**\n     * openImage opens a file, detect its mime-type and create an image resource from it\n     * @param \\Symfony\\Component\\HttpFoundation\\File\\File $file\n     * @return mixed\n     */\n    protected function openImage($file): ImageInterface\n    {\n        $filePath = $file->getPathname();\n\n        $driver = new \\Intervention\\Image\\Drivers\\Gd\\Driver;\n\n        $manager = new \\Intervention\\Image\\ImageManager($driver);\n\n        return $manager->read($filePath);\n    }\n\n    /**\n     * reset the image back to the original.\n     */\n    public function reset(): static\n    {\n        $this->image = $this->openImage($this->file);\n\n        return $this;\n    }\n\n    /**\n     * save the image based on its file type.\n     * @param string $savePath\n     */\n    public function save($savePath)\n    {\n        $this->image->save(\n            $savePath,\n            ...$this->buildEncoderOptions($savePath)\n        );\n    }\n\n    /**\n     * resize and/or crop an image, specifying the new width and height of the\n     * destination image.\n     * @param int|null $width\n     * @param int|null $height\n     * @param array $options\n     */\n    public function resize($width, $height, $options = []): static\n    {\n        $this->setOptions($options);\n\n        // Support null for proportional resizing\n        $width = (int) $width;\n        $height = (int) $height;\n\n        if (!$width && !$height) {\n            $width = $this->width;\n            $height = $this->height;\n        }\n        elseif (!$width) {\n            $width = (int) round($height * ($this->width / $this->height));\n        }\n        elseif (!$height) {\n            $height = (int) round($width * ($this->height / $this->width));\n        }\n\n        $mode = $this->options['mode'] ?? 'auto';\n\n        if ($mode === 'exact') {\n            $this->image->resize($width, $height);\n        }\n        elseif ($mode === 'crop') {\n            // Backward compatibility\n            if (!is_array($options['offset'] ?? null)) {\n                $this->image->cover($width, $height);\n            }\n            else {\n                $this->image->crop(\n                    $width,\n                    $height,\n                    $this->options['offset'][0] ?? ($this->options['offset']['x'] ?? 0),\n                    $this->options['offset'][1] ?? ($this->options['offset']['y'] ?? 0)\n                );\n            }\n        }\n        elseif ($mode === 'cover') {\n            $this->image->cover($width, $height);\n        }\n        elseif ($mode === 'fit') {\n            $this->image->scale($width, $height);\n        }\n        elseif ($mode === 'auto') {\n            $this->image->scale($width, $height);\n        }\n        elseif ($mode === 'portrait') {\n            $this->image->scale(null, $height);\n        }\n        elseif ($mode === 'landscape') {\n            $this->image->scale($width, null);\n        }\n\n        $sharpen = $this->getOption('sharpen');\n        if ($sharpen > 0) {\n            $this->image->sharpen($sharpen);\n        }\n\n        return $this;\n    }\n\n    /**\n     * buildEncoderOptions builds encoder options (quality, interlace/progressive) for saving.\n     * @param string $savePath\n     */\n    protected function buildEncoderOptions(string $savePath): array\n    {\n        $encoderOptions = [];\n\n        $quality = $this->getOption('quality');\n        if ($quality !== null) {\n            $encoderOptions['quality'] = (int) $quality;\n        }\n\n        // Interlace option maps to 'interlaced' for PNG/GIF and 'progressive' for JPEG\n        $interlace = $this->getOption('interlace');\n        if ($interlace) {\n            $extension = strtolower(pathinfo($savePath, PATHINFO_EXTENSION));\n            if ($extension === 'jpg' || $extension === 'jpeg') {\n                $encoderOptions['progressive'] = true;\n            }\n            elseif ($extension === 'png' || $extension === 'gif') {\n                $encoderOptions['interlaced'] = true;\n            }\n        }\n\n        return $encoderOptions;\n    }\n}\n"
  },
  {
    "path": "src/Router/CoreRedirector.php",
    "content": "<?php namespace October\\Rain\\Router;\n\nuse App;\nuse Illuminate\\Routing\\Redirector as RedirectorBase;\n\n/**\n * CoreRedirector adds extra events to the base redirector and ensures the \"intended\"\n * session is different for the frontend and backend contexts.\n *\n * @package october\\router\n * @author Alexey Bobkov, Samuel Georges\n */\nclass CoreRedirector extends RedirectorBase\n{\n    /**\n     * intended creates a new redirect response to the previously intended location.\n     * @return \\Illuminate\\Http\\RedirectResponse\n     */\n    public function intended($default = '/', $status = 302, $headers = [], $secure = null)\n    {\n        if (!App::runningInFrontend()) {\n            return parent::intended($default, $status, $headers, $secure);\n        }\n\n        $path = $this->session->pull('url.cms.intended', $default);\n\n        return $this->to($path, $status, $headers, $secure);\n    }\n\n    /**\n     * getIntendedUrl from the session.\n     */\n    public function getIntendedUrl()\n    {\n        if (!App::runningInFrontend()) {\n            return parent::getIntendedUrl();\n        }\n\n        return $this->session->get('url.cms.intended');\n    }\n\n    /**\n     * setIntendedUrl in the session.\n     */\n    public function setIntendedUrl($url)\n    {\n        if (!App::runningInFrontend()) {\n            return parent::setIntendedUrl($url);\n        }\n\n        $this->session->put('url.cms.intended', $url);\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Router/CoreRouter.php",
    "content": "<?php namespace October\\Rain\\Router;\n\nuse Illuminate\\Http\\Request;\nuse Illuminate\\Routing\\Router as RouterBase;\n\n/**\n * CoreRouter adds extra events to the base router and ensures late routes\n * are registered with the caching system.\n *\n * @package october\\router\n * @author Alexey Bobkov, Samuel Georges\n */\nclass CoreRouter extends RouterBase\n{\n    /**\n     * @var bool routerEventsBooted\n     */\n    protected $routerEventsBooted = false;\n\n    /**\n     * dispatch the request to the application.\n     *\n     * @param  \\Illuminate\\Http\\Request  $request\n     * @return \\Illuminate\\Http\\Response|\\Illuminate\\Http\\JsonResponse\n     */\n    public function dispatch(Request $request)\n    {\n        $this->currentRequest = $request;\n\n        $this->events->dispatch('router.before', [$request]);\n\n        $response = $this->dispatchToRoute($request);\n\n        $this->events->dispatch('router.after', [$request, $response]);\n\n        return $response;\n    }\n\n    /**\n     * before is a new filter registered with the router.\n     *\n     * @param  string|callable  $callback\n     * @return void\n     */\n    public function before($callback)\n    {\n        $this->events->listen('router.before', $callback);\n    }\n\n    /**\n     * after is a new filter registered with the router.\n     *\n     * @param  string|callable  $callback\n     * @return void\n     */\n    public function after($callback)\n    {\n        $this->events->listen('router.after', $callback);\n    }\n\n    /**\n     * registerLateRoutes found within \"before\" filter, some are registered here.\n     *\n     * @param  \\Illuminate\\Http\\Request  $request\n     * @return void\n     */\n    public function registerLateRoutes()\n    {\n        if (!$this->routerEventsBooted) {\n            $this->events->dispatch('router.before', [new Request]);\n        }\n\n        $this->routerEventsBooted = true;\n\n        return $this;\n    }\n}\n"
  },
  {
    "path": "src/Router/Helper.php",
    "content": "<?php namespace October\\Rain\\Router;\n\n/**\n * Helper methods that may be useful for processing routing activity\n *\n * @package october\\router\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Helper\n{\n    /**\n     * validateUrl checks if the URL pattern provided is valid for parsing\n     */\n    public static function validateUrl(string $url): bool\n    {\n        if ($url && $url[0] !==  '/') {\n            return false;\n        }\n\n        $segments = static::segmentizeUrl($url);\n\n        foreach ($segments as $segment) {\n            // Remove regex portion\n            $cleanSegment = explode('|', $segment)[0];\n\n            // Validate segment\n            if (!preg_match(\n                '/^[a-z0-9\\/\\:_\\-\\*\\[\\]\\+\\?\\.\\^\\\\\\$]*$/i',\n                $cleanSegment\n            )) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * normalizeUrl adds leading slash and removes trailing slash from the URL.\n     *\n     * @param string $url URL to normalize.\n     * @return string Returns normalized URL.\n     */\n    public static function normalizeUrl($url)\n    {\n        if (substr($url, 0, 1) !== '/') {\n            $url = '/'.$url;\n        }\n\n        if (substr($url, -1) === '/') {\n            $url = substr($url, 0, -1);\n        }\n\n        if (!strlen($url)) {\n            $url = '/';\n        }\n\n        return $url;\n    }\n\n    /**\n     * segmentizeUrl splits a URL by segments separated by the slash symbol\n     * and returns the URL segments. Using a pattern includes regex support\n     * in the URL.\n     *\n     * @param string $url\n     * @param bool $pattern\n     * @return array\n     */\n    public static function segmentizeUrl($url, $pattern = true)\n    {\n        $url = self::normalizeUrl($url);\n\n        if ($pattern) {\n            $segments = preg_split(\"#(?<!\\\\\\)/#\", $url);\n        }\n        else {\n            $segments = explode('/', $url);\n        }\n\n        $result = [];\n        foreach ($segments as $segment) {\n            if (strlen($segment)) {\n                $result[] = $segment;\n            }\n        }\n\n        return $result;\n    }\n\n    /**\n     * rebuildUrl from an array of segments.\n     *\n     * @param array $urlArray Array the URL segments.\n     * @return string Returns rebuilt URL.\n     */\n    public static function rebuildUrl(array $urlArray)\n    {\n        $url = '';\n        foreach ($urlArray as $segment) {\n            if (strlen($segment)) {\n                $url .= '/'.trim($segment);\n            }\n        }\n\n        return self::normalizeUrl($url);\n    }\n\n    /**\n     * parseValues replaces :column_name with it's object value.\n     * Example: /some/link/:id/:name -> /some/link/1/Joe\n     *\n     * @param stdObject $object Object containing the data\n     * @param array $columns Expected key names to parse\n     * @param string $string URL template\n     * @return string Built string\n     */\n    public static function parseValues($object, array $columns, $string)\n    {\n        if (is_array($object)) {\n            $object = (object) $object;\n        }\n\n        foreach ($columns as $column) {\n            if (\n                !isset($object->{$column}) ||\n                is_array($object->{$column}) ||\n                (is_object($object->{$column}) && !method_exists($object->{$column}, '__toString'))\n            ) {\n                continue;\n            }\n\n            $string = str_replace(':'.$column, urlencode((string) $object->{$column}), $string);\n        }\n\n        return $string;\n    }\n\n    /**\n     * replaceParameters replaces :column_name with object value without requiring a\n     * list of names. Example: /some/link/:id/:name -> /some/link/1/Joe\n     *\n     * @param stdObject $object Object containing the data\n     * @param string $string URL template\n     * @return string Built string\n     */\n    public static function replaceParameters($object, $string)\n    {\n        if (preg_match_all('/\\:([\\w]+)/', $string, $matches)) {\n            return self::parseValues($object, $matches[1], $string);\n        }\n\n        return $string;\n    }\n\n    /**\n     * segmentIsWildcard checks whether an URL pattern segment is a wildcard.\n     * @param string $segment The segment definition.\n     * @return boolean Returns boolean true if the segment is a wildcard. Returns false otherwise.\n     */\n    public static function segmentIsWildcard($segment)\n    {\n        $name = mb_substr($segment, 1);\n\n        $wildMarkerPos = mb_strpos($name, '*');\n        if ($wildMarkerPos === false) {\n            return false;\n        }\n\n        $regexMarkerPos = mb_strpos($name, '|');\n        if ($regexMarkerPos === false) {\n            return true;\n        }\n\n        if ($wildMarkerPos !== false && $regexMarkerPos !== false) {\n            return $wildMarkerPos < $regexMarkerPos;\n        }\n\n        return true;\n    }\n\n    /**\n     * segmentIsOptional checks whether an URL pattern segment is optional.\n     * @param string $segment The segment definition.\n     * @return boolean Returns boolean true if the segment is optional. Returns false otherwise.\n     */\n    public static function segmentIsOptional($segment)\n    {\n        $name = mb_substr($segment, 1);\n\n        $optMarkerPos = mb_strpos($name, '?');\n        if ($optMarkerPos === false) {\n            return false;\n        }\n\n        $regexMarkerPos = mb_strpos($name, '|');\n        if ($regexMarkerPos === false) {\n            return true;\n        }\n\n        if ($optMarkerPos !== false && $regexMarkerPos !== false) {\n            return $optMarkerPos < $regexMarkerPos;\n        }\n\n        return false;\n    }\n\n    /**\n     * getParameterName extracts the parameter name from a URL pattern segment definition.\n     * @param string $segment\n     * @return string\n     */\n    public static function getParameterName($segment)\n    {\n        $name = mb_substr($segment, 1);\n\n        $regexMarkerPos = mb_strpos($name, '|');\n        if ($regexMarkerPos !== false) {\n            $name = mb_substr($name, 0, $regexMarkerPos);\n        }\n\n        $optMarkerPos = mb_strpos($name, '?');\n        if ($optMarkerPos !== false) {\n            $name = mb_substr($name, 0, $optMarkerPos);\n        }\n\n        $wildMarkerPos = mb_strpos($name, '*');\n        if ($wildMarkerPos !== false) {\n            $name = mb_substr($name, 0, $wildMarkerPos);\n        }\n\n        return $name;\n    }\n\n    /**\n     * getSegmentRegExp extracts the regular expression from a URL pattern segment definition.\n     * @param string $segment The segment definition.\n     * @return string Returns the regular expression string or false if the expression is not defined.\n     */\n    public static function getSegmentRegExp($segment)\n    {\n        if (($pos = mb_strpos($segment, '|')) !== false) {\n            $regexp = mb_substr($segment, $pos+1);\n            if (!mb_strlen($regexp)) {\n                return false;\n            }\n\n            return '/'.$regexp.'/';\n        }\n\n        return false;\n    }\n\n    /**\n     * getSegmentDefaultValue extracts the default parameter value from a URL pattern\n     * segment definition.\n     * @param string $segment The segment definition.\n     * @return string Returns the default value if it is provided. Returns false otherwise.\n     */\n    public static function getSegmentDefaultValue($segment)\n    {\n        $regexMarkerPos = mb_strpos($segment, '|');\n\n        // Find the '?' that acts as the optional marker, not one inside a regex\n        $optMarkerPos = mb_strpos($segment, '?');\n        if ($optMarkerPos === false) {\n            return false;\n        }\n\n        // If '|' comes before '?', the '?' is part of the regex, not an optional marker\n        if ($regexMarkerPos !== false && $regexMarkerPos < $optMarkerPos) {\n            return false;\n        }\n\n        $value = false;\n\n        if ($regexMarkerPos !== false) {\n            $value = mb_substr($segment, $optMarkerPos+1, $regexMarkerPos-$optMarkerPos-1);\n        }\n        else {\n            $value = mb_substr($segment, $optMarkerPos+1);\n        }\n\n        // Filter out wildcard marker (it's a modifier, not a default value)\n        if ($value === '*') {\n            return false;\n        }\n\n        return strlen($value) ? $value : false;\n    }\n}\n"
  },
  {
    "path": "src/Router/README.md",
    "content": "# URL Router\n\nURL route patterns follow an easy to read syntax and use in-place named parameters, so there is no need to use regular expressions in most cases.\n\n## Creating a route\n\nYou should prepare your route like so:\n\n```php\n$router = new Router;\n\n// New route with ID: myRouteId\n$router->route('myRouteId', '/post/:id');\n\n// New route with ID: anotherRouteId\n$router->route('anotherRouteId', '/profile/:username');\n```\n\n## Route matching\n\nOnce you have prepared your route you can match it like this:\n\n```php\nif ($router->match('/post/2')) {\n\n    // Returns: [id => 2]\n    $params = $router->getParameters(); \n\n    // Returns: myRouteId\n    $routeId = $router->matchedRoute(); \n}\n```\n\n## Reverse matching\n\nYou can also reverse match a route by it's identifier:\n\n```php\n// Returns: /post/2\n$url = $router->url('myRouteId', ['id' => 2]);\n```"
  },
  {
    "path": "src/Router/Router.php",
    "content": "<?php namespace October\\Rain\\Router;\n\n/**\n * Router used in October CMS for managing page routes.\n *\n * @package october\\router\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Router\n{\n    /**\n     * @var string defaultValue to use when a required parameter is not specified\n     */\n    public static $defaultValue = 'default';\n\n    /**\n     * @var array routeMap is a list of specified routes\n     */\n    protected $routeMap = [];\n\n    /**\n     * @var \\October\\Rain\\Router\\Rule matchedRouteRule reference\n     */\n    protected $matchedRouteRule;\n\n    /**\n     * @var array parameters with names and values extracted from the URL pattern and URL string\n     */\n    protected $parameters = [];\n\n    /**\n     * route registers a new route rule\n     */\n    public function route($name, $route)\n    {\n        return $this->routeMap[$name] = Rule::fromPattern($name, $route);\n    }\n\n    /**\n     * match given URL string\n     * @param string $url Request URL to match for\n     * @return bool\n     */\n    public function match($url)\n    {\n        // Reset any previous matches\n        $this->matchedRouteRule = null;\n\n        $segments = Helper::segmentizeUrl($url, false);\n\n        $parameters = [];\n        foreach ($this->routeMap as $routeRule) {\n            if ($routeRule->resolveUrlSegments($segments, $parameters)) {\n                $this->matchedRouteRule = $routeRule;\n\n                // If this route has a condition, run it\n                $callback = $routeRule->condition();\n                if ($callback !== null) {\n                    $callbackResult = call_user_func($callback, $parameters, Helper::normalizeUrl($url));\n\n                    // Callback responded to abort\n                    if ($callbackResult === false) {\n                        $parameters = [];\n                        $this->matchedRouteRule = null;\n                        continue;\n                    }\n                }\n\n                break;\n            }\n        }\n\n        // Success\n        if ($this->matchedRouteRule) {\n            // If this route has a match callback, run it\n            $matchCallback = $routeRule->afterMatch();\n            if ($matchCallback !== null) {\n                $parameters = call_user_func($matchCallback, $parameters, $url);\n            }\n        }\n\n        $this->parameters = $parameters;\n\n        return $this->matchedRouteRule ? true : false;\n    }\n\n    /**\n     * url builds a URL together by matching route name and supplied parameters\n     *\n     * @param string $name Name of the route previously defined.\n     * @param array $parameters Parameter name => value items to fill in for given route.\n     * @return string Full matched URL as string with given values put in place of named parameters\n     */\n    public function url($name, $parameters = [])\n    {\n        if (!isset($this->routeMap[$name])) {\n            return null;\n        }\n\n        $routeRule = $this->routeMap[$name];\n\n        $pattern = $routeRule->pattern();\n\n        return $this->urlFromPattern($pattern, $parameters);\n    }\n\n    /**\n     * urlFromPattern builds a URL together by matching route pattern and supplied parameters\n     *\n     * @param string $pattern Route pattern string, eg: /path/to/something/:parameter\n     * @param array $parameters Parameter name => value items to fill in for given route.\n     * @return string Full matched URL as string with given values put in place of named parameters\n     */\n    public function urlFromPattern($pattern, $parameters = [])\n    {\n        $patternSegments = Helper::segmentizeUrl($pattern);\n\n        // Normalize the parameters, colons (:) in key names are removed.\n        //\n        foreach ($parameters as $param => $value) {\n            if (strpos($param, ':') !== 0) {\n                continue;\n            }\n            $normalizedParam = substr($param, 1);\n            $parameters[$normalizedParam] = $value;\n            unset($parameters[$param]);\n        }\n\n        // Build the URL segments, remember the last populated index\n        //\n        $url = [];\n        $lastPopulatedIndex = 0;\n\n        foreach ($patternSegments as $index => $patternSegment) {\n            // Static segment\n            if (strpos($patternSegment, ':') !== 0) {\n                $url[] = $patternSegment;\n            }\n            // Dynamic segment\n            else {\n                $paramName = Helper::getParameterName($patternSegment);\n\n                // Determine whether it is optional\n                $optional = Helper::segmentIsOptional($patternSegment);\n\n                // Default value\n                $defaultValue = Helper::getSegmentDefaultValue($patternSegment);\n\n                // Check if parameter has been supplied and is not a default value\n                $parameterExists = isset($parameters[$paramName]) &&\n                    strlen($parameters[$paramName]) &&\n                    $parameters[$paramName] !== $defaultValue;\n\n                // Use supplied parameter value\n                if ($parameterExists) {\n                    $url[] = $parameters[$paramName];\n                }\n                // Look for a specified default value\n                elseif ($optional) {\n                    $url[] = $defaultValue ?: static::$defaultValue;\n\n                    // Do not set $lastPopulatedIndex\n                    continue;\n                }\n                // Non optional field, use the default value\n                else {\n                    $url[] = static::$defaultValue;\n                }\n            }\n\n            $lastPopulatedIndex = $index;\n        }\n\n        // Trim the URL to only include populated segments\n        $url = array_slice($url, 0, $lastPopulatedIndex + 1);\n\n        return Helper::rebuildUrl($url);\n    }\n\n    /**\n     * getRouteMap returns the active list of router rule objects\n     * @return array An associative array with keys matching the route rule names and\n     * values matching the router rule object.\n     */\n    public function getRouteMap()\n    {\n        return $this->routeMap;\n    }\n\n    /**\n     * getParameters returns a list of parameters specified in the requested page URL.\n     * For example, if the URL pattern was /blog/post/:id and the actual URL\n     * was /blog/post/10, the $parameters['id'] element would be 10.\n     * @return array An associative array with keys matching the parameter names specified in the URL pattern and\n     * values matching the corresponding segments of the actual requested URL.\n     */\n    public function getParameters()\n    {\n        return $this->parameters;\n    }\n\n    /**\n     * matchedRoute returns the matched route rule name.\n     * @return \\October\\Rain\\Router\\Rule The matched rule object.\n     */\n    public function matchedRoute()\n    {\n        if (!$this->matchedRouteRule) {\n            return false;\n        }\n\n        return $this->matchedRouteRule->name();\n    }\n\n    /**\n     * reset clears all existing routes\n     * @return $this\n     */\n    public function reset()\n    {\n        $this->routeMap = [];\n        return $this;\n    }\n\n    /**\n     * sortRules sorts all the routing rules by static segments (long to short),\n     * then dynamic segments (short to long), then wild segments (at end).\n     * @return void\n     */\n    public function sortRules()\n    {\n        uasort($this->routeMap, function ($a, $b) {\n            // When comparing static, longer tails go to the start\n            $lengthA = $a->staticSegmentCount;\n            $lengthB = $b->staticSegmentCount;\n\n            if ($lengthA > $lengthB) {\n                return -1;\n            }\n\n            if ($lengthA < $lengthB) {\n                return 1;\n            }\n\n            // When static tails are equal, push wilds to the end\n            $lengthA = $a->wildSegmentCount;\n            $lengthB = $b->wildSegmentCount;\n\n            if ($lengthA > $lengthB) {\n                return 1;\n            }\n\n            if ($lengthA < $lengthB) {\n                return -1;\n            }\n\n            // When comparing dynamic, longer tails go to the end\n            $lengthA = $a->dynamicSegmentCount;\n            $lengthB = $b->dynamicSegmentCount;\n\n            if ($lengthA > $lengthB) {\n                return 1;\n            }\n\n            if ($lengthA < $lengthB) {\n                return -1;\n            }\n\n            return 0;\n        });\n    }\n\n    /**\n     * fromArray loads routes from an array.\n     */\n    public function fromArray($routes)\n    {\n        foreach ($routes as $route) {\n            $this->routeMap[$route['ruleName']] = new Rule($route);\n        }\n    }\n\n    /**\n     * toArray converts the rules to an array.\n     * @return array\n     */\n    public function toArray()\n    {\n        $this->sortRules();\n\n        $rules = [];\n\n        foreach ($this->routeMap as $rule) {\n            $rules[] = $rule->toArray();\n        }\n\n        return $rules;\n    }\n}\n"
  },
  {
    "path": "src/Router/RoutingServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Router;\n\nuse Illuminate\\Routing\\RoutingServiceProvider as RoutingServiceProviderBase;\n\n/**\n * RoutingServiceProvider\n *\n * @package october\\router\n * @author Alexey Bobkov, Samuel Georges\n */\nclass RoutingServiceProvider extends RoutingServiceProviderBase\n{\n    /**\n     * boot any application services.\n     */\n    public function boot()\n    {\n        if ($this->app->routesAreCached()) {\n            $this->loadCachedRoutes();\n        }\n        else {\n            $this->app->booted(function () {\n                $this->app['router']->getRoutes()->refreshNameLookups();\n                $this->app['router']->getRoutes()->refreshActionLookups();\n            });\n        }\n    }\n\n    /**\n     * loadCachedRoutes for the application.\n     */\n    protected function loadCachedRoutes()\n    {\n        $this->app->booted(function () {\n            require $this->app->getCachedRoutesPath();\n        });\n    }\n\n    /**\n     * registerRouter instance.\n     */\n    protected function registerRouter()\n    {\n        $this->app->singleton('router', function ($app) {\n            return new CoreRouter($app['events'], $app);\n        });\n    }\n\n    /**\n     * registerRedirector\n     */\n    protected function registerRedirector()\n    {\n        $this->app->singleton('redirect', function ($app) {\n            $redirector = new CoreRedirector($app['url']);\n\n            // If the session is set on the application instance, we'll inject it into\n            // the redirector instance. This allows the redirect responses to allow\n            // for the quite convenient \"with\" methods that flash to the session.\n            if (isset($app['session.store'])) {\n                $redirector->setSession($app['session.store']);\n            }\n\n            return $redirector;\n        });\n    }\n}\n"
  },
  {
    "path": "src/Router/Rule.php",
    "content": "<?php namespace October\\Rain\\Router;\n\nuse InvalidArgumentException;\nuse Exception;\n\n/**\n * Rule object for routes\n *\n * @package october\\router\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Rule\n{\n    /**\n     * @var array config values for this instance\n     */\n    protected $config = [];\n\n    /**\n     * @var string ruleName is a named reference for this rule.\n     */\n    protected $ruleName;\n\n    /**\n     * @var string rulePattern used to match this rule.\n     */\n    protected $rulePattern;\n\n    /**\n     * @var function conditionCallback used when matching this rule.\n     */\n    protected $conditionCallback;\n\n    /**\n     * @var function afterMatchCallback called when this rule is matched.\n     */\n    protected $afterMatchCallback;\n\n    /**\n     * @var string staticUrl with static segments only, dynamic segments are stripped\n     */\n    public $staticUrl;\n\n    /**\n     * @var array segments for the pattern\n     */\n    public $segments;\n\n    /**\n     * @var int segmentCount is the number of segments in the pattern\n     */\n    public $segmentCount = 0;\n\n    /**\n     * @var array staticSegments are the static segments\n     */\n    public $staticSegments = 0;\n\n    /**\n     * @var int staticSegmentCount the number of static segments found in the pattern\n     */\n    public $staticSegmentCount = 0;\n\n    /**\n     * @var int dynamicSegmentCount the number of dynamic segments found in the pattern\n     */\n    public $dynamicSegmentCount = 0;\n\n    /**\n     * @var int wildSegmentCount the number of wildcard segments found in the pattern\n     */\n    public $wildSegmentCount = 0;\n\n    /**\n     * __construct the new router rule instance.\n     *\n     * @param string $name\n     * @param string $pattern\n     */\n    public function __construct($config = [])\n    {\n        $this->config = $config;\n\n        foreach ($config as $key => $val) {\n            $this->{$key} = $val;\n        }\n    }\n\n    /**\n     * fromPattern returns a named rule from a pattern\n     */\n    public static function fromPattern($name, $pattern): static\n    {\n        $segments = Helper::segmentizeUrl($pattern);\n\n        // Create the static URL for this pattern for reverse lookup\n        //\n        $staticSegments = [];\n        $staticSegmentCount = $dynamicSegmentCount = $wildSegmentCount = 0;\n\n        foreach ($segments as $segment) {\n            if (strpos($segment, ':') !== 0) {\n                $staticSegments[] = $segment;\n                $staticSegmentCount++;\n            }\n            else {\n                $dynamicSegmentCount++;\n\n                if (Helper::segmentIsWildcard($segment)) {\n                    $wildSegmentCount++;\n                }\n            }\n        }\n\n        $staticUrl = Helper::rebuildUrl($staticSegments);\n\n        // Build and return rule\n        //\n        $rule = new static([\n            'ruleName' => $name,\n            'rulePattern' => $pattern,\n            'segments' => $segments,\n            'segmentCount' => count($segments),\n            'staticUrl' => $staticUrl,\n            'staticSegments' => $staticSegments,\n            'staticSegmentCount' => $staticSegmentCount,\n            'dynamicSegmentCount' => $dynamicSegmentCount,\n            'wildSegmentCount' => $wildSegmentCount,\n        ]);\n\n        return $rule;\n    }\n\n    /**\n     * resolveUrl checks whether a given URL matches a given pattern, with a reference to a PHP array\n     * variable to return the parameter list fetched from URL. Returns true if the URL matches the\n     * pattern. Otherwise returns false.\n     * @param string $url\n     * @param array $parameters\n     * @return bool\n     */\n    public function resolveUrl($url, &$parameters)\n    {\n        return $this->resolveUrlSegments(Helper::segmentizeUrl($url), $parameters);\n    }\n\n    /**\n     * resolveUrlSegments is an internal method used for multiple checks.\n     * @param array $urlSegments\n     * @param array $parameters\n     * @return bool\n     */\n    public function resolveUrlSegments($urlSegments, &$parameters)\n    {\n        $parameters = [];\n\n        // Only one wildcard can be used, if found, pull out the excess segments\n        $wildSegments = [];\n        if ($this->wildSegmentCount === 1) {\n            $wildSegments = $this->captureWildcardSegments($urlSegments);\n        }\n\n        // If the number of URL segments is more than the number of pattern segments\n        if (count($urlSegments) > $this->segmentCount) {\n            return false;\n        }\n\n        // Compare pattern and URL segments\n        foreach ($this->segments as $index => $patternSegment) {\n            // Static segment\n            if (strpos($patternSegment, ':') !== 0) {\n                if (\n                    !array_key_exists($index, $urlSegments) ||\n                    mb_strtolower($patternSegment) !== mb_strtolower($urlSegments[$index])\n                ) {\n                    return false;\n                }\n            }\n            // Dynamic segment\n            else {\n                // Initialize the parameter\n                $paramName = Helper::getParameterName($patternSegment);\n                $parameters[$paramName] = false;\n\n                // Determine whether it is optional\n                $optional = Helper::segmentIsOptional($patternSegment);\n\n                // Check if the optional segment has no required segments following it\n                if ($optional && $index < ($this->segmentCount - 1)) {\n                    for ($i = $index+1; $i < $this->segmentCount; $i++) {\n                        if (!Helper::segmentIsOptional($this->segments[$i])) {\n                            $optional = false;\n                            break;\n                        }\n                    }\n                }\n\n                // If the segment is optional and there is no corresponding value in the URL,\n                // assign the default value (if provided) and skip to the next segment.\n                $urlSegmentExists = array_key_exists($index, $urlSegments);\n\n                if ($optional && !$urlSegmentExists) {\n                    $parameters[$paramName] = Helper::getSegmentDefaultValue($patternSegment);\n                    continue;\n                }\n\n                // If the segment is not optional and there is no corresponding value in the URL\n                if (!$optional && !$urlSegmentExists) {\n                    return false;\n                }\n\n                // Validate the value with the regular expression\n                $regexp = Helper::getSegmentRegExp($patternSegment);\n\n                if ($regexp) {\n                    try {\n                        if (!preg_match($regexp, $urlSegments[$index])) {\n                            return false;\n                        }\n                    }\n                    catch (Exception $ex) {\n                    }\n                }\n\n                // Set the parameter value\n                $parameters[$paramName] = $urlSegments[$index];\n\n                // Determine if wildcard and add stored parameters as a suffix\n                if (Helper::segmentIsWildcard($patternSegment) && count($wildSegments)) {\n                    $parameters[$paramName] .= Helper::rebuildUrl($wildSegments);\n                }\n            }\n        }\n\n        return true;\n    }\n\n    /**\n     * captureWildcardSegments captures and removes every segment of a URL after a wildcard\n     * pattern segment is detected, until both collections of segments\n     * are the same size.\n     * @param array $urlSegments\n     * @return array\n     */\n    protected function captureWildcardSegments(&$urlSegments)\n    {\n        $wildSegments = [];\n        $patternSegments = $this->segments;\n        $segmentDiff = count($urlSegments) - count($patternSegments);\n        $wildMode = false;\n        $wildCount = 0;\n\n        foreach ($urlSegments as $index => $urlSegment) {\n            if ($wildMode) {\n                if ($wildCount < $segmentDiff) {\n                    $wildSegments[] = $urlSegment;\n                    $wildCount++;\n                    unset($urlSegments[$index]);\n                    continue;\n                }\n\n                break;\n            }\n\n            $patternSegment = $patternSegments[$index];\n            if (Helper::segmentIsWildcard($patternSegment)) {\n                $wildMode = true;\n            }\n        }\n\n        // Reset array index\n        $urlSegments = array_values($urlSegments);\n\n        return $wildSegments;\n    }\n\n    /**\n     * name is a unique route name\n     *\n     * @param string $name Unique name for the router object\n     * @return object Self\n     */\n    public function name($name = null)\n    {\n        if ($name === null) {\n            return $this->ruleName;\n        }\n\n        $this->ruleName = $name;\n\n        return $this;\n    }\n\n    /**\n     * pattern for the route match\n     *\n     * @param string $pattern Pattern used to match this rule\n     * @return self\n     */\n    public function pattern($pattern = null)\n    {\n        if ($pattern === null) {\n            return $this->rulePattern;\n        }\n\n        $this->rulePattern = $pattern;\n\n        return $this;\n    }\n\n    /**\n     * condition callback\n     *\n     * @param callback $callback Callback function to be used when providing custom route match conditions\n     * @throws InvalidArgumentException When supplied argument is not a valid callback\n     * @return callback\n     */\n    public function condition($callback = null)\n    {\n        if ($callback !== null) {\n            if (!is_callable($callback)) {\n                throw new InvalidArgumentException(sprintf(\n                    \"Condition provided is not a valid callback. Given (%s)\",\n                    gettype($callback)\n                ));\n            }\n\n            $this->conditionCallback = $callback;\n\n            return $this;\n        }\n\n        return $this->conditionCallback;\n    }\n\n    /**\n     * afterMatch callback\n     *\n     * @param callback $callback Callback function to be used to modify params after a successful match\n     * @throws InvalidArgumentException When supplied argument is not a valid callback\n     * @return callback\n     */\n    public function afterMatch($callback = null)\n    {\n        if ($callback !== null) {\n            if (!is_callable($callback)) {\n                throw new InvalidArgumentException(sprintf(\n                    \"The after match callback provided is not valid. Given (%s)\",\n                    gettype($callback)\n                ));\n            }\n\n            $this->afterMatchCallback = $callback;\n\n            return $this;\n        }\n\n        return $this->afterMatchCallback;\n    }\n\n    /**\n     * toArray\n     */\n    public function toArray()\n    {\n        return $this->config;\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/CreateCommand.php",
    "content": "<?php namespace October\\Rain\\Scaffold\\Console;\n\nuse October\\Rain\\Scaffold\\GeneratorCommandBase;\n\n/**\n * CreateCommand\n */\nclass CreateCommand extends GeneratorCommandBase\n{\n    /**\n     * @var string signature for the command\n     */\n    protected $signature = 'create:command\n        {namespace : App or Plugin Namespace. <info>(eg: Acme.Blog)</info>}\n        {name : The name of the command. Eg: ProcessJobs}\n        {--o|overwrite : Overwrite existing files with generated ones}';\n\n    /**\n     * @var string description of the console command\n     */\n    protected $description = 'Creates a new console command.';\n\n    /**\n     * @var string type of class being generated\n     */\n    protected $typeLabel = 'Command';\n\n    /**\n     * makeStubs makes all stubs\n     */\n    public function makeStubs()\n    {\n        $this->makeStub('command/command.stub', 'console/{{studly_name}}.php');\n    }\n\n    /**\n     * prepareVars prepares variables for stubs\n     */\n    protected function prepareVars(): array\n    {\n        return [\n            'name' => $this->argument('name'),\n            'namespace' => $this->argument('namespace'),\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/CreateComponent.php",
    "content": "<?php namespace October\\Rain\\Scaffold\\Console;\n\nuse October\\Rain\\Scaffold\\GeneratorCommandBase;\n\n/**\n * CreateComponent\n */\nclass CreateComponent extends GeneratorCommandBase\n{\n    /**\n     * @var string signature for the command\n     */\n    protected $signature = 'create:component\n        {namespace : App or Plugin Namespace. <info>(eg: Acme.Blog)</info>}\n        {name : The name of the component. Eg: Posts}\n        {--o|overwrite : Overwrite existing files with generated ones}';\n\n    /**\n     * @var string name of console command\n     */\n    protected $name = 'create:component';\n\n    /**\n     * @var string description of the console command\n     */\n    protected $description = 'Creates a new plugin component.';\n\n    /**\n     * @var string type of class being generated\n     */\n    protected $typeLabel = 'Component';\n\n    /**\n     * makeStubs makes all stubs\n     */\n    public function makeStubs()\n    {\n        $this->makeStub('component/component.stub', 'components/{{studly_name}}.php');\n        $this->makeStub('component/default.stub', 'components/{{lower_name}}/default.htm');\n    }\n\n    /**\n     * prepareVars prepares variables for stubs\n     */\n    protected function prepareVars(): array\n    {\n        return [\n            'name' => $this->argument('name'),\n            'namespace' => $this->argument('namespace'),\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/CreateContentField.php",
    "content": "<?php namespace October\\Rain\\Scaffold\\Console;\n\nuse October\\Rain\\Scaffold\\GeneratorCommandBase;\n\n/**\n * CreateContentField\n */\nclass CreateContentField extends GeneratorCommandBase\n{\n    /**\n     * @var string signature for the command\n     */\n    protected $signature = 'create:contentfield\n        {namespace : App or Plugin Namespace. <info>(eg: Acme.Blog)</info>}\n        {name : The name of the content field. Eg: IconPicker}\n        {--o|overwrite : Overwrite existing files with generated ones}';\n\n    /**\n     * @var string description of the console command\n     */\n    protected $description = 'Creates a new content field.';\n\n    /**\n     * @var string type of class being generated\n     */\n    protected $typeLabel = 'Content Field';\n\n    /**\n     * makeStubs makes all stubs\n     */\n    public function makeStubs()\n    {\n        $this->makeStub('contentfield/contentfield.stub', 'contentfields/{{studly_name}}.php');\n    }\n\n    /**\n     * prepareVars prepares variables for stubs\n     */\n    protected function prepareVars(): array\n    {\n        return [\n            'name' => $this->argument('name'),\n            'namespace' => $this->argument('namespace'),\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/CreateController.php",
    "content": "<?php namespace October\\Rain\\Scaffold\\Console;\n\nuse October\\Rain\\Scaffold\\GeneratorCommandBase;\nuse October\\Rain\\Support\\Str;\n\n/**\n * CreateController\n */\nclass CreateController extends GeneratorCommandBase\n{\n    /**\n     * @var string signature for the command\n     */\n    protected $signature = 'create:controller\n        {namespace : App or Plugin Namespace. <info>(eg: Acme.Blog)</info>}\n        {name : The name of the controller. Eg: Posts}\n        {--model= : Define which model name to use, otherwise the singular controller name is used.}\n        {--design= : Specify a design (basic, sidebar, survey, popup, custom)}\n        {--no-form : Do not implement a form for this controller}\n        {--no-list : Do not implement a list for this controller}\n        {--o|overwrite : Overwrite existing files with generated ones}';\n\n    /**\n     * @var string description of the console command\n     */\n    protected $description = 'Creates a new controller.';\n\n    /**\n     * @var string typeLabel of class being generated\n     */\n    protected $typeLabel = 'Controller';\n\n    /**\n     * makeStubs makes all stubs\n     */\n    public function makeStubs()\n    {\n        $design = $this->defineDesignMode();\n\n        $this->makeStub('controller/controller.stub', 'controllers/{{studly_name}}.php');\n\n        if (!$this->option('no-list')) {\n            $this->makeStub('controller/config_list.stub', 'controllers/{{lower_name}}/config_list.yaml');\n            $this->makeStub('controller/_list_toolbar.stub', 'controllers/{{lower_name}}/_list_toolbar.php');\n            $this->makeStub('controller/index.stub', 'controllers/{{lower_name}}/index.php');\n        }\n\n        if (!$this->option('no-form')) {\n            $this->makeStub('controller/config_form.stub', 'controllers/{{lower_name}}/config_form.yaml');\n            if (in_array($design, ['basic', 'sidebar', 'survey'])) {\n                $this->makeStub('controller/update_design.stub', 'controllers/{{lower_name}}/update.php');\n                $this->makeStub('controller/preview_design.stub', 'controllers/{{lower_name}}/preview.php');\n                $this->makeStub('controller/create_design.stub', 'controllers/{{lower_name}}/create.php');\n            }\n            elseif ($design !== 'popup') {\n                $this->makeStub('controller/update.stub', 'controllers/{{lower_name}}/update.php');\n                $this->makeStub('controller/preview.stub', 'controllers/{{lower_name}}/preview.php');\n                $this->makeStub('controller/create.stub', 'controllers/{{lower_name}}/create.php');\n            }\n        }\n    }\n\n    /**\n     * prepareVars prepares variables for stubs\n     */\n    protected function prepareVars(): array\n    {\n        return [\n            'name' => $this->argument('name'),\n            'namespace' => $this->argument('namespace'),\n            'model' => $this->defineModelName(),\n            'design' => $this->defineDesignMode(),\n            'form' => !$this->option('no-form'),\n            'list' => !$this->option('no-list'),\n        ];\n    }\n\n    /**\n     * defineModelName to use, either supplied or singular from the controller name\n     */\n    protected function defineModelName(): string\n    {\n        $model = $this->option('model');\n\n        if (!$model) {\n            $model = Str::singular($this->argument('name'));\n        }\n\n        return $model;\n\n    }\n\n    /**\n     * defineDesignMode\n     */\n    protected function defineDesignMode(): string\n    {\n        if ($design = $this->option('design')) {\n            return trim(strtolower($design));\n        }\n\n        return '';\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/CreateFactory.php",
    "content": "<?php namespace October\\Rain\\Scaffold\\Console;\n\nuse October\\Rain\\Scaffold\\GeneratorCommandBase;\n\n/**\n * CreateFactory\n */\nclass CreateFactory extends GeneratorCommandBase\n{\n    /**\n     * @var string signature for the command\n     */\n    protected $signature = 'create:factory\n        {namespace : App or Plugin Namespace. <info>(eg: Acme.Blog)</info>}\n        {name : The name of the factory class to generate. <info>(eg: PostFactory)</info>}\n        {--o|overwrite : Overwrite existing files with generated ones}';\n\n    /**\n     * @var string description of the console command\n     */\n    protected $description = 'Creates a new factory class.';\n\n    /**\n     * @var string typeLabel of class being generated\n     */\n    protected $typeLabel = 'Factory';\n\n    /**\n     * handle executes the console command\n     */\n    public function handle()\n    {\n        if (!str_ends_with($this->argument('name'), 'Factory')) {\n            $this->components->error('Factory classes names must end in \"Factory\"');\n            return;\n        }\n\n        parent::handle();\n    }\n\n    /**\n     * makeStubs makes all stubs\n     */\n    public function makeStubs()\n    {\n        if ($this->isAppNamespace()) {\n            $this->makeStub('factory/factory_app.stub', 'database/factories/{{studly_name}}.php');\n        }\n        else {\n            $this->makeStub('factory/factory.stub', 'updates/factories/{{studly_name}}.php');\n        }\n    }\n\n    /**\n     * prepareVars prepares variables for stubs\n     */\n    protected function prepareVars(): array\n    {\n        return [\n            'name' => $this->argument('name'),\n            'namespace' => $this->argument('namespace'),\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/CreateFilterWidget.php",
    "content": "<?php namespace October\\Rain\\Scaffold\\Console;\n\nuse October\\Rain\\Scaffold\\GeneratorCommandBase;\n\n/**\n * CreateFilterWidget\n */\nclass CreateFilterWidget extends GeneratorCommandBase\n{\n    /**\n     * @var string signature for the command\n     */\n    protected $signature = 'create:filterwidget\n        {namespace : App or Plugin Namespace. <info>(eg: Acme.Blog)</info>}\n        {name : The name of the filter widget. Eg: HasDiscount}\n        {--o|overwrite : Overwrite existing files with generated ones}';\n\n    /**\n     * @var string name of console command\n     */\n    protected $name = 'create:filterwidget';\n\n    /**\n     * @var string description of the console command\n     */\n    protected $description = 'Creates a new filter widget.';\n\n    /**\n     * @var string type of class being generated\n     */\n    protected $typeLabel = 'Filter Widget';\n\n    /**\n     * makeStubs makes all stubs\n     */\n    public function makeStubs()\n    {\n        $this->makeStub('filterwidget/filterwidget.stub', 'filterwidgets/{{studly_name}}.php');\n        $this->makeStub('filterwidget/partial.stub', 'filterwidgets/{{lower_name}}/partials/_{{lower_name}}.php');\n        $this->makeStub('filterwidget/partial_form.stub', 'filterwidgets/{{lower_name}}/partials/_{{lower_name}}_form.php');\n        $this->makeStub('filterwidget/stylesheet.stub', 'filterwidgets/{{lower_name}}/assets/css/{{lower_name}}.css');\n        $this->makeStub('filterwidget/javascript.stub', 'filterwidgets/{{lower_name}}/assets/js/{{lower_name}}.js');\n    }\n\n    /**\n     * prepareVars prepares variables for stubs\n     */\n    protected function prepareVars(): array\n    {\n        return [\n            'name' => $this->argument('name'),\n            'namespace' => $this->argument('namespace'),\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/CreateFormWidget.php",
    "content": "<?php namespace October\\Rain\\Scaffold\\Console;\n\nuse October\\Rain\\Scaffold\\GeneratorCommandBase;\n\n/**\n * CreateFormWidget\n */\nclass CreateFormWidget extends GeneratorCommandBase\n{\n    /**\n     * @var string signature for the command\n     */\n    protected $signature = 'create:formwidget\n        {namespace : App or Plugin Namespace. <info>(eg: Acme.Blog)</info>}\n        {name : The name of the form widget. Eg: PostList}\n        {--o|overwrite : Overwrite existing files with generated ones}';\n\n    /**\n     * @var string description of the console command\n     */\n    protected $description = 'Creates a new form widget.';\n\n    /**\n     * @var string type of class being generated\n     */\n    protected $typeLabel = 'Form Widget';\n\n    /**\n     * makeStubs makes all stubs\n     */\n    public function makeStubs()\n    {\n        $this->makeStub('formwidget/formwidget.stub', 'formwidgets/{{studly_name}}.php');\n        $this->makeStub('formwidget/partial.stub', 'formwidgets/{{lower_name}}/partials/_{{lower_name}}.php');\n        $this->makeStub('formwidget/stylesheet.stub', 'formwidgets/{{lower_name}}/assets/css/{{lower_name}}.css');\n        $this->makeStub('formwidget/javascript.stub', 'formwidgets/{{lower_name}}/assets/js/{{lower_name}}.js');\n    }\n\n    /**\n     * prepareVars prepares variables for stubs\n     */\n    protected function prepareVars(): array\n    {\n        return [\n            'name' => $this->argument('name'),\n            'namespace' => $this->argument('namespace'),\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/CreateJob.php",
    "content": "<?php namespace October\\Rain\\Scaffold\\Console;\n\nuse October\\Rain\\Scaffold\\GeneratorCommandBase;\n\n/**\n * CreateJob\n */\nclass CreateJob extends GeneratorCommandBase\n{\n    /**\n     * @var string signature for the command\n     */\n    protected $signature = 'create:job\n        {namespace : App or Plugin Namespace. <info>(eg: Acme.Blog)</info>}\n        {name : The name of the job class to generate. <info>(eg: ImportPosts)</info>}\n        {--s|sync : Indicates that job should be synchronous}\n        {--o|overwrite : Overwrite existing files with generated ones}';\n\n    /**\n     * @var string description of the console command\n     */\n    protected $description = 'Creates a new job class.';\n\n    /**\n     * @var string typeLabel of class being generated\n     */\n    protected $typeLabel = 'Job';\n\n    /**\n     * makeStubs makes all stubs\n     */\n    public function makeStubs()\n    {\n        if ($this->option('sync')) {\n            $this->makeStub('job/job.stub', 'jobs/{{studly_name}}.php');\n        }\n        else {\n            $this->makeStub('job/job.queued.stub', 'jobs/{{studly_name}}.php');\n        }\n    }\n\n    /**\n     * prepareVars prepares variables for stubs\n     */\n    protected function prepareVars(): array\n    {\n        return [\n            'name' => $this->argument('name'),\n            'namespace' => $this->argument('namespace'),\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/CreateMigration.php",
    "content": "<?php namespace October\\Rain\\Scaffold\\Console;\n\nuse Str;\nuse October\\Rain\\Scaffold\\GeneratorCommandBase;\n\n/**\n * CreateMigration\n */\nclass CreateMigration extends GeneratorCommandBase\n{\n    /**\n     * @var string signature for the command\n     */\n    protected $signature = 'create:migration\n        {namespace : App or Plugin Namespace. <info>(eg: Acme.Blog)</info>}\n        {name : The name of the model. Eg: Post}\n        {--create= : The table to be created}\n        {--table= : The table to migrate}\n        {--soft-deletes : Implement soft deletion on this model}\n        {--no-timestamps : Disable auto-timestamps on this model}\n        {--o|overwrite : Overwrite existing files with generated ones}';\n\n    /**\n     * @var string description of the console command\n     */\n    protected $description = 'Creates a new migration.';\n\n    /**\n     * @var string type of class being generated\n     */\n    protected $typeLabel = 'Migration';\n\n    /**\n     * @var bool isCreate determines if this is a creation migration\n     */\n    protected $isCreate = false;\n\n    /**\n     * makeStubs makes all stubs\n     */\n    public function makeStubs()\n    {\n        if ($this->isAppNamespace()) {\n            if ($this->isCreate) {\n                $this->makeStub('migration/create_app_table.stub', 'database/migrations/'.$this->getDatePrefix().'_{{snake_name}}.php');\n            }\n            else {\n                $this->makeStub('migration/update_app_table.stub', 'database/migrations/'.$this->getDatePrefix().'_{{snake_name}}.php');\n            }\n        }\n        else {\n            if ($this->isCreate) {\n                $this->makeStub('migration/create_table.stub', 'updates/{{snake_name}}.php');\n            }\n            else {\n                $this->makeStub('migration/update_table.stub', 'updates/{{snake_name}}.php');\n            }\n        }\n    }\n\n    /**\n     * prepareVars prepares variables for stubs\n     */\n    protected function prepareVars(): array\n    {\n        return [\n            'name' => $this->argument('name'),\n            'namespace' => $this->argument('namespace'),\n            'table' => $this->defineTableName(),\n            'softDeletes' => $this->option('soft-deletes'),\n            'timestamps' => !$this->option('no-timestamps')\n        ];\n    }\n\n    /**\n     * defineTableName\n     */\n    protected function defineTableName(): string\n    {\n        if ($table = $this->option('table')) {\n            return $table;\n        }\n\n        if ($table = $this->option('create')) {\n            $this->isCreate = true;\n            return $table;\n        }\n\n        return $this->guessTableName();\n    }\n\n    /**\n     * guessTableName\n     */\n    protected function guessTableName(): string\n    {\n        $tableName = Str::snake($this->argument('name'));\n\n        $createPatterns = [\n            '/^create_(\\w+)_table$/',\n            '/^create_(\\w+)$/',\n        ];\n\n        foreach ($createPatterns as $pattern) {\n            if (preg_match($pattern, $tableName, $matches)) {\n                $tableName = $matches[1];\n                $this->isCreate = true;\n            }\n        }\n\n        $updatePatterns = [\n            '/_(to|from|in)_(\\w+)_table$/',\n            '/_(to|from|in)_(\\w+)$/',\n        ];\n\n        foreach ($updatePatterns as $pattern) {\n            if (preg_match($pattern, $tableName, $matches)) {\n                $tableName = $matches[1];\n            }\n        }\n\n        return $this->getNamespaceTable() . '_' .$tableName;\n    }\n\n    /**\n     * getDatePrefix\n     * @return string\n     */\n    protected function getDatePrefix()\n    {\n        return date('Y_m_d_His');\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/CreateModel.php",
    "content": "<?php namespace October\\Rain\\Scaffold\\Console;\n\nuse October\\Rain\\Scaffold\\GeneratorCommandBase;\n\n/**\n * CreateModel\n */\nclass CreateModel extends GeneratorCommandBase\n{\n    /**\n     * @var string signature for the command\n     */\n    protected $signature = 'create:model\n        {namespace : App or Plugin Namespace. <info>(eg: Acme.Blog)</info>}\n        {name : The name of the model. Eg: Post}\n        {--soft-deletes : Implement soft deletion on this model}\n        {--no-timestamps : Disable auto-timestamps on this model}\n        {--no-migration : Do not generate a migration file for this model}\n        {--o|overwrite : Overwrite existing files with generated ones}';\n\n    /**\n     * @var string description of the console command\n     */\n    protected $description = 'Creates a new model.';\n\n    /**\n     * @var string type of class being generated\n     */\n    protected $typeLabel = 'Model';\n\n    /**\n     * makeStubs makes all stubs\n     */\n    public function makeStubs()\n    {\n        $this->makeStub('model/model.stub', 'models/{{studly_name}}.php');\n        $this->makeStub('model/fields.stub', 'models/{{lower_name}}/fields.yaml');\n        $this->makeStub('model/columns.stub', 'models/{{lower_name}}/columns.yaml');\n\n        if (!$this->option('no-migration')) {\n            $this->call('create:migration', array_filter([\n                'name' => 'Create'.$this->vars['studly_plural_name'].'Table',\n                'namespace' => $this->argument('namespace'),\n                '--create' => $this->vars['namespace_table'].'_'.$this->vars['snake_plural_name'],\n                '--soft-deletes' => $this->option('soft-deletes'),\n                '--no-timestamps' => $this->option('no-timestamps'),\n                '--overwrite' => $this->option('overwrite')\n            ]));\n        }\n    }\n\n    /**\n     * prepareVars prepares variables for stubs\n     */\n    protected function prepareVars(): array\n    {\n        return [\n            'name' => $this->argument('name'),\n            'namespace' => $this->argument('namespace'),\n            'softDeletes' => $this->option('soft-deletes'),\n            'timestamps' => !$this->option('no-timestamps')\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/CreatePlugin.php",
    "content": "<?php namespace October\\Rain\\Scaffold\\Console;\n\nuse October\\Rain\\Scaffold\\GeneratorCommandBase;\n\nclass CreatePlugin extends GeneratorCommandBase\n{\n    /**\n     * @var string signature for the command\n     */\n    protected $signature = 'create:plugin\n        {namespace : The name of the plugin to create. <info>(eg: Acme.Blog)</info>}\n        {--o|overwrite : Overwrite existing files with generated ones}';\n\n    /**\n     * @var string description of the console command\n     */\n    protected $description = 'Creates a new plugin.';\n\n    /**\n     * @var string type of class being generated\n     */\n    protected $typeLabel = 'Plugin';\n\n    /**\n     * makeStubs makes all stubs\n     */\n    public function makeStubs()\n    {\n        $this->makeStub('plugin/plugin.stub', 'Plugin.php');\n        $this->makeStub('plugin/version.stub', 'updates/version.yaml');\n        $this->makeStub('plugin/composer.stub', 'composer.json');\n    }\n\n    /**\n     * prepareVars prepares variables for stubs\n     */\n    protected function prepareVars(): array\n    {\n        if (!$this->validateInput()) {\n            exit(1);\n        }\n\n        return [\n            'namespace' => $this->argument('namespace'),\n        ];\n    }\n\n    protected function validateInput()\n    {\n        if ($this->isAppNamespace()) {\n            $this->error('Cannot create plugin in app namespace');\n            return false;\n        }\n\n        // Extract the author and name from the plugin code\n        $pluginCode = $this->argument('namespace');\n        $parts = explode('.', $pluginCode);\n\n        if (count($parts) !== 2) {\n            $this->error('Invalid plugin name, either too many dots or not enough.');\n            $this->error('Example name: AuthorName.PluginName');\n            return false;\n        }\n\n        return true;\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/CreateReportWidget.php",
    "content": "<?php namespace October\\Rain\\Scaffold\\Console;\n\nuse October\\Rain\\Scaffold\\GeneratorCommandBase;\n\n/**\n * CreateReportWidget\n */\nclass CreateReportWidget extends GeneratorCommandBase\n{\n    /**\n     * @var string signature for the command\n     */\n    protected $signature = 'create:reportwidget\n        {namespace : App or Plugin Namespace. <info>(eg: Acme.Blog)</info>}\n        {name : The name of the report widget. Eg: TopPages}\n        {--o|overwrite : Overwrite existing files with generated ones}';\n\n    /**\n     * @var string description of the console command\n     */\n    protected $description = 'Creates a new report widget.';\n\n    /**\n     * @var string type of class being generated\n     */\n    protected $typeLabel = 'Report Widget';\n\n    /**\n     * makeStubs makes all stubs\n     */\n    public function makeStubs()\n    {\n        $this->makeStub('reportwidget/reportwidget.stub', 'reportwidgets/{{studly_name}}.php');\n        $this->makeStub('reportwidget/widget.stub', 'reportwidgets/{{lower_name}}/partials/_{{lower_name}}.php');\n    }\n\n    /**\n     * prepareVars prepares variables for stubs\n     */\n    protected function prepareVars(): array\n    {\n        return [\n            'name' => $this->argument('name'),\n            'namespace' => $this->argument('namespace'),\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/CreateSeeder.php",
    "content": "<?php namespace October\\Rain\\Scaffold\\Console;\n\nuse October\\Rain\\Scaffold\\GeneratorCommandBase;\n\n/**\n * CreateSeeder\n */\nclass CreateSeeder extends GeneratorCommandBase\n{\n    /**\n     * @var string signature for the command\n     */\n    protected $signature = 'create:seeder\n        {namespace : App or Plugin Namespace. <info>(eg: Acme.Blog)</info>}\n        {name : The name of the job class to generate. <info>(eg: PostSeeder)</info>}\n        {--o|overwrite : Overwrite existing files with generated ones}';\n\n    /**\n     * @var string description of the console command\n     */\n    protected $description = 'Creates a new seeder class.';\n\n    /**\n     * @var string typeLabel of class being generated\n     */\n    protected $typeLabel = 'Seeder';\n\n    /**\n     * makeStubs makes all stubs\n     */\n    public function makeStubs()\n    {\n        if ($this->isAppNamespace()) {\n            $this->makeStub('seeder/create_app_seeder.stub', 'database/seeders/{{studly_name}}.php');\n        } else {\n            $this->makeStub('seeder/create_seeder.stub', 'updates/seeders/{{studly_name}}.php');\n        }\n    }\n\n    /**\n     * prepareVars prepares variables for stubs\n     */\n    protected function prepareVars(): array\n    {\n        return [\n            'name' => $this->argument('name'),\n            'namespace' => $this->argument('namespace'),\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/CreateTest.php",
    "content": "<?php namespace October\\Rain\\Scaffold\\Console;\n\nuse October\\Rain\\Scaffold\\GeneratorCommandBase;\n\n/**\n * CreateTest\n */\nclass CreateTest extends GeneratorCommandBase\n{\n    /**\n     * @var string signature for the command\n     */\n    protected $signature = 'create:test\n        {namespace : App or Plugin Namespace. <info>(eg: Acme.Blog)</info>}\n        {name : The name of the test class to generate. <info>(eg: UserTest)</info>}\n        {--o|overwrite : Overwrite existing files with generated ones}';\n\n    /**\n     * @var string description of the console command\n     */\n    protected $description = 'Creates a new test class.';\n\n    /**\n     * @var string typeLabel of class being generated\n     */\n    protected $typeLabel = 'Test';\n\n    /**\n     * handle executes the console command\n     */\n    public function handle()\n    {\n        if (!str_ends_with($this->argument('name'), 'Test')) {\n            $this->components->error('Test classes names must end in \"Test\"');\n            return;\n        }\n\n        parent::handle();\n    }\n\n    /**\n     * makeStubs makes all stubs\n     */\n    public function makeStubs()\n    {\n        if (!file_exists($this->getDestinationPath() . '/phpunit.xml')) {\n            if ($this->isAppNamespace()) {\n                $this->makeStub('test/phpunit.app.stub', 'phpunit.xml');\n            }\n            else {\n                $this->makeStub('test/phpunit.plugin.stub', 'phpunit.xml');\n            }\n        }\n\n        $this->makeStub('test/test.stub', 'tests/{{studly_name}}.php');\n    }\n\n    /**\n     * prepareVars prepares variables for stubs\n     */\n    protected function prepareVars(): array\n    {\n        return [\n            'name' => $this->argument('name'),\n            'namespace' => $this->argument('namespace'),\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/command/command.stub",
    "content": "<?php namespace {{namespace_php}}\\Console;\n\nuse Illuminate\\Console\\Command;\n\n/**\n * {{studly_name}} Command\n *\n * @link https://docs.octobercms.com/4.x/extend/console-commands.html\n */\nclass {{studly_name}} extends Command\n{\n    /**\n     * @var string signature for the console command.\n     */\n    protected $signature = '{{lower_plugin}}:{{lower_name}} {user}';\n\n    /**\n     * @var string description is the console command description\n     */\n    protected $description = 'No description provided yet...';\n\n    /**\n     * handle executes the console command.\n     */\n    public function handle()\n    {\n        $username = $this->argument('user');\n\n        $this->output->writeln(\"Hello {$username}!\");\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/component/component.stub",
    "content": "<?php namespace {{namespace_php}}\\Components;\n\nuse Cms\\Classes\\ComponentBase;\n\n/**\n * {{studly_name}} Component\n *\n * @link https://docs.octobercms.com/4.x/extend/cms-components.html\n */\nclass {{studly_name}} extends ComponentBase\n{\n    public function componentDetails()\n    {\n        return [\n            'name' => '{{title_name}} Component',\n            'description' => 'No description provided yet...'\n        ];\n    }\n\n    /**\n     * @link https://docs.octobercms.com/4.x/element/inspector-types.html\n     */\n    public function defineProperties()\n    {\n        return [];\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/component/default.stub",
    "content": "<p>This is the default markup for component {{name}}</p>\n\n<small>You can delete this file if you want</small>\n"
  },
  {
    "path": "src/Scaffold/Console/contentfield/contentfield.stub",
    "content": "<?php namespace {{namespace_php}}\\ContentFields;\n\nuse Tailor\\Classes\\ContentFieldBase;\nuse October\\Contracts\\Element\\FormElement;\nuse October\\Contracts\\Element\\ListElement;\nuse October\\Contracts\\Element\\FilterElement;\n\n/**\n * {{studly_name}} Content Field\n *\n * @link https://docs.octobercms.com/4.x/extend/tailor-fields.html\n */\nclass {{studly_name}} extends ContentFieldBase\n{\n    /**\n     * defineFormField will define how a field is displayed in a form.\n     */\n    public function defineFormField(FormElement $form, $context = null)\n    {\n        $form->addFormField($this->fieldName, $this->label)->useConfig($this->config)->displayAs('text');\n    }\n\n    /**\n     * defineListColumn will define how a field is displayed in a list.\n     */\n    public function defineListColumn(ListElement $list, $context = null)\n    {\n        $list->defineColumn($this->fieldName, $this->label)->displayAs('switch');\n    }\n\n    /**\n     * defineFilterScope will define how a field is displayed in a filter.\n     */\n    public function defineFilterScope(FilterElement $filter, $context = null)\n    {\n        $filter->defineScope($this->fieldName, $this->label)->displayAs('switch');\n    }\n\n    /**\n     * extendModelObject will extend the record model.\n     */\n    public function extendModelObject($model)\n    {\n        $model->addJsonable($this->fieldName);\n    }\n\n    /**\n     * extendDatabaseTable adds any required columns to the database.\n     */\n    public function extendDatabaseTable($table)\n    {\n        $table->mediumText($this->fieldName)->nullable();\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/controller/_list_toolbar.stub",
    "content": "<div data-control=\"toolbar loader-container\">\n{% if design == 'popup' %}\n    <button\n        type=\"button\"\n        data-control=\"popup\"\n        data-handler=\"onLoadPopupForm\"\n        class=\"btn btn-primary\">\n        <i class=\"icon-plus\"></i>\n        <?= __(\"New :name\", ['name' => '{{title_singular_name}}']) ?>\n    </button>\n{% else %}\n    <a\n        href=\"<?= Backend::url('{{namespace_path}}/{{lower_name}}/create') ?>\"\n        class=\"btn btn-primary\">\n        <i class=\"icon-plus\"></i>\n        <?= __(\"New :name\", ['name' => '{{title_singular_name}}']) ?>\n    </a>\n{% endif %}\n\n    <div class=\"toolbar-divider\"></div>\n\n    <button\n        class=\"btn btn-secondary\"\n        data-request=\"onDelete\"\n        data-request-message=\"<?= __(\"Deleting...\") ?>\"\n        data-request-confirm=\"<?= __(\"Are you sure?\") ?>\"\n        data-list-checked-trigger\n        data-list-checked-request\n        disabled>\n        <i class=\"icon-delete\"></i>\n        <?= __(\"Delete\") ?>\n    </button>\n</div>\n"
  },
  {
    "path": "src/Scaffold/Console/controller/config_form.stub",
    "content": "# ===================================\n#  Form Behavior Config\n# ===================================\n\n# Record name\nname: {{title_singular_name}}\n\n# Model Form Field configuration\nform: {{namespace_local}}/models/{{lower_model}}/fields.yaml\n\n# Model Class name\nmodelClass: {{namespace_php}}\\Models\\{{studly_model}}\n\n# Default redirect location\ndefaultRedirect: {{namespace_path}}/{{lower_name}}\n\n{% if design %}\n# Form Design\ndesign:\n    displayMode: {{design}}\n\n{% endif %}\n# Create page\ncreate:\n    title: backend::lang.form.create_title\n    redirect: {{namespace_path}}/{{lower_name}}/update/:id\n    redirectClose: {{namespace_path}}/{{lower_name}}\n\n# Update page\nupdate:\n    title: backend::lang.form.update_title\n    redirect: {{namespace_path}}/{{lower_name}}\n    redirectClose: {{namespace_path}}/{{lower_name}}\n\n# Preview page\npreview:\n    title: backend::lang.form.preview_title\n"
  },
  {
    "path": "src/Scaffold/Console/controller/config_list.stub",
    "content": "# ===================================\n#  List Behavior Config\n# ===================================\n\n# Model List Column configuration\nlist: {{namespace_local}}/models/{{lower_model}}/columns.yaml\n\n# Model Class name\nmodelClass: {{namespace_php}}\\Models\\{{studly_model}}\n\n# List Title\ntitle: Manage {{title_plural_name}}\n\n{% if design == 'popup' %}\n# Link each record to popup form design\nrecordOnClick: popup\n{% else %}\n# Link URL for each record\nrecordUrl: {{namespace_path}}/{{lower_name}}/update/:id\n{% endif %}\n\n# Message to display if the list is empty\nnoRecordsMessage: backend::lang.list.no_records\n\n# Records to display per page\nrecordsPerPage: 20\n\n# Display page numbers with pagination, disable to improve performance\nshowPageNumbers: true\n\n# Displays the list column set up button\nshowSetup: true\n\n# Displays the sorting link on each column\nshowSorting: true\n\n# Default sorting column\ndefaultSort:\n    column: id\n    direction: asc\n\n# Display checkboxes next to each record\nshowCheckboxes: true\n\n# Toolbar widget configuration\ntoolbar:\n    # Partial for toolbar buttons\n    buttons: list_toolbar\n\n    # Search widget configuration\n    search:\n        prompt: backend::lang.list.search_prompt\n\n# List Structure (Optional)\n# structure:\n#     # Allow collapsing of tree items\n#     showTree: false\n#\n#     # Allow items to be reordered\n#     showReorder: true\n#\n#     # Maximum reordering depth\n#     maxDepth: 2\n"
  },
  {
    "path": "src/Scaffold/Console/controller/controller.stub",
    "content": "<?php namespace {{namespace_php}}\\Controllers;\n\nuse BackendMenu;\nuse Backend\\Classes\\Controller;\n\n/**\n * {{title_name}} Backend Controller\n *\n * @link https://docs.octobercms.com/4.x/extend/system/controllers.html\n */\nclass {{studly_name}} extends Controller\n{\n{% if form or list %}\n    public $implement = [\n{% if form %}\n        \\Backend\\Behaviors\\FormController::class,\n{% endif %}{% if list %}\n        \\Backend\\Behaviors\\ListController::class,\n{% endif %}\n    ];\n{% endif %}{% if form %}\n\n    /**\n     * @var string formConfig file\n     */\n    public $formConfig = 'config_form.yaml';\n{% endif %}{% if list %}\n\n    /**\n     * @var string listConfig file\n     */\n    public $listConfig = 'config_list.yaml';\n{% endif %}\n\n    /**\n     * @var array required permissions\n     */\n    public $requiredPermissions = ['{{lower_namespace_code}}.{{lower_name}}'];\n\n    /**\n     * __construct the controller\n     */\n    public function __construct()\n    {\n        parent::__construct();\n\n        BackendMenu::setContext('{{namespace_code}}', '{{lower_plugin}}', '{{lower_name}}');\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/controller/create.stub",
    "content": "<?php Block::put('breadcrumb') ?>\n    <ol class=\"breadcrumb\">\n        <li class=\"breadcrumb-item\"><a href=\"<?= Backend::url('{{namespace_path}}/{{lower_name}}') ?>\">{{title_name}}</a></li>\n        <li class=\"breadcrumb-item active\" aria-current=\"page\"><?= e($this->pageTitle) ?></li>\n    </ol>\n<?php Block::endPut() ?>\n\n<?php if (!$this->fatalError): ?>\n\n    <?= Form::open(['class' => 'd-flex flex-column h-100']) ?>\n\n        <div class=\"flex-grow-1\">\n            <?= $this->formRender() ?>\n        </div>\n\n        <div class=\"form-buttons\">\n            <div data-control=\"loader-container\">\n                <button\n                    type=\"submit\"\n                    data-request=\"onSave\"\n                    data-request-message=\"<?= __(\"Creating :name...\", ['name' => $formRecordName]) ?>\"\n                    data-hotkey=\"ctrl+s, cmd+s\"\n                    class=\"btn btn-primary\">\n                    <?= __(\"Create\") ?>\n                </button>\n                <button\n                    type=\"button\"\n                    data-request=\"onSave\"\n                    data-request-data=\"{ close: 1 }\"\n                    data-request-message=\"<?= __(\"Creating :name...\", ['name' => $formRecordName]) ?>\"\n                    data-hotkey=\"ctrl+enter, cmd+enter\"\n                    class=\"btn btn-default\">\n                    <?= __(\"Create & Close\") ?>\n                </button>\n                <span class=\"btn-text\">\n                    <span class=\"button-separator\"><?= __(\"or\") ?></span>\n                    <a\n                        href=\"<?= Backend::url('{{namespace_path}}/{{lower_name}}') ?>\"\n                        class=\"btn btn-link p-0\">\n                        <?= __(\"Cancel\") ?>\n                    </a>\n                </span>\n            </div>\n        </div>\n\n    <?= Form::close() ?>\n\n<?php else: ?>\n\n    <p class=\"flash-message static error\">\n        <?= e($this->fatalError) ?>\n    </p>\n    <p>\n        <a\n            href=\"<?= Backend::url('{{namespace_path}}/{{lower_name}}') ?>\"\n            class=\"btn btn-default\">\n            <?= __(\"Return to List\") ?>\n        </a>\n    </p>\n\n<?php endif ?>\n"
  },
  {
    "path": "src/Scaffold/Console/controller/create_design.stub",
    "content": "<?php Block::put('breadcrumb') ?>\n    <ol class=\"breadcrumb\">\n        <li class=\"breadcrumb-item\"><a href=\"<?= Backend::url('{{namespace_path}}/{{lower_name}}') ?>\">{{title_name}}</a></li>\n        <li class=\"breadcrumb-item active\" aria-current=\"page\"><?= e($this->pageTitle) ?></li>\n    </ol>\n<?php Block::endPut() ?>\n\n<?= $this->formRenderDesign() ?>\n"
  },
  {
    "path": "src/Scaffold/Console/controller/index.stub",
    "content": "\n<?= $this->listRender() ?>\n"
  },
  {
    "path": "src/Scaffold/Console/controller/preview.stub",
    "content": "<?php Block::put('breadcrumb') ?>\n    <ol class=\"breadcrumb\">\n        <li class=\"breadcrumb-item\"><a href=\"<?= Backend::url('{{namespace_path}}/{{lower_name}}') ?>\">{{title_name}}</a></li>\n        <li class=\"breadcrumb-item active\" aria-current=\"page\"><?= e($this->pageTitle) ?></li>\n    </ol>\n<?php Block::endPut() ?>\n\n<?php if (!$this->fatalError): ?>\n\n    <div class=\"form-preview\">\n        <?= $this->formRenderPreview() ?>\n    </div>\n\n<?php else: ?>\n\n    <p class=\"flash-message static error\"><?= e($this->fatalError) ?></p>\n    <p><a href=\"<?= Backend::url('{{namespace_path}}/{{lower_name}}') ?>\" class=\"btn btn-default\"><?= e(trans('backend::lang.form.return_to_list')) ?></a></p>\n\n<?php endif ?>\n"
  },
  {
    "path": "src/Scaffold/Console/controller/preview_design.stub",
    "content": "<?php Block::put('breadcrumb') ?>\n    <ol class=\"breadcrumb\">\n        <li class=\"breadcrumb-item\"><a href=\"<?= Backend::url('{{namespace_path}}/{{lower_name}}') ?>\">{{title_name}}</a></li>\n        <li class=\"breadcrumb-item active\" aria-current=\"page\"><?= e($this->pageTitle) ?></li>\n    </ol>\n<?php Block::endPut() ?>\n\n<?= $this->formRenderDesign() ?>\n"
  },
  {
    "path": "src/Scaffold/Console/controller/update.stub",
    "content": "<?php Block::put('breadcrumb') ?>\n    <ol class=\"breadcrumb\">\n        <li class=\"breadcrumb-item\"><a href=\"<?= Backend::url('{{namespace_path}}/{{lower_name}}') ?>\">{{title_name}}</a></li>\n        <li class=\"breadcrumb-item active\" aria-current=\"page\"><?= e($this->pageTitle) ?></li>\n    </ol>\n<?php Block::endPut() ?>\n\n<?php if (!$this->fatalError): ?>\n\n    <?= Form::open(['class' => 'd-flex flex-column h-100']) ?>\n\n        <div class=\"flex-grow-1\">\n            <?= $this->formRender() ?>\n        </div>\n\n        <div class=\"form-buttons\">\n            <div data-control=\"loader-container\">\n                <button\n                    type=\"submit\"\n                    data-request=\"onSave\"\n                    data-request-data=\"{ redirect: 0 }\"\n                    data-hotkey=\"ctrl+s, cmd+s\"\n                    data-request-message=\"<?= __(\"Saving :name...\", ['name' => $formRecordName]) ?>\"\n                    class=\"btn btn-primary\">\n                    <?= __(\"Save\") ?>\n                </button>\n                <button\n                    type=\"button\"\n                    data-request=\"onSave\"\n                    data-request-data=\"{ close: 1 }\"\n                    data-browser-redirect-back\n                    data-hotkey=\"ctrl+enter, cmd+enter\"\n                    data-request-message=\"<?= __(\"Saving :name...\", ['name' => $formRecordName]) ?>\"\n                    class=\"btn btn-default\">\n                    <?= __(\"Save & Close\") ?>\n                </button>\n                <button\n                    type=\"button\"\n                    class=\"oc-icon-delete btn-icon danger pull-right\"\n                    data-request=\"onDelete\"\n                    data-request-message=\"<?= __(\"Deleting :name...\", ['name' => $formRecordName]) ?>\"\n                    data-request-confirm=\"<?= __(\"Delete this record?\") ?>\">\n                </button>\n                <span class=\"btn-text\">\n                    <span class=\"button-separator\"><?= __(\"or\") ?></span>\n                    <a\n                        href=\"<?= Backend::url('{{namespace_path}}/{{lower_name}}') ?>\"\n                        class=\"btn btn-link p-0\">\n                        <?= __(\"Cancel\") ?>\n                    </a>\n                </span>\n            </div>\n        </div>\n\n    <?= Form::close() ?>\n\n<?php else: ?>\n\n    <p class=\"flash-message static error\">\n        <?= e($this->fatalError) ?>\n    </p>\n    <p>\n        <a\n            href=\"<?= Backend::url('{{namespace_path}}/{{lower_name}}') ?>\"\n            class=\"btn btn-default\">\n            <?= __(\"Return to List\") ?>\n        </a>\n    </p>\n\n<?php endif ?>\n"
  },
  {
    "path": "src/Scaffold/Console/controller/update_design.stub",
    "content": "<?php Block::put('breadcrumb') ?>\n    <ol class=\"breadcrumb\">\n        <li class=\"breadcrumb-item\"><a href=\"<?= Backend::url('{{namespace_path}}/{{lower_name}}') ?>\">{{title_name}}</a></li>\n        <li class=\"breadcrumb-item active\" aria-current=\"page\"><?= e($this->pageTitle) ?></li>\n    </ol>\n<?php Block::endPut() ?>\n\n<?= $this->formRenderDesign() ?>\n"
  },
  {
    "path": "src/Scaffold/Console/factory/factory.stub",
    "content": "<?php namespace {{namespace_php}}\\Updates\\Factories;\n\nuse October\\Rain\\Database\\Factories\\Factory;\n\n/**\n * {{studly_name}}\n */\nclass {{studly_name}} extends Factory\n{\n    /**\n     * definition for the default state.\n     * @return array<string, mixed>\n     */\n    public function definition()\n    {\n        return [\n            //\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/factory/factory_app.stub",
    "content": "<?php namespace {{namespace_php}}\\Database\\Factories;\n\nuse October\\Rain\\Database\\Factories\\Factory;\n\n/**\n * {{studly_name}}\n */\nclass {{studly_name}} extends Factory\n{\n    /**\n     * definition for the default state.\n     * @return array<string, mixed>\n     */\n    public function definition()\n    {\n        return [\n            //\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/filterwidget/filterwidget.stub",
    "content": "<?php namespace {{namespace_php}}\\FilterWidgets;\n\nuse Backend\\Classes\\FilterWidgetBase;\n\n/**\n * {{studly_name}} Filter Widget\n *\n * @link https://docs.octobercms.com/4.x/extend/lists/filter-widgets.html\n */\nclass {{studly_name}} extends FilterWidgetBase\n{\n    public function init()\n    {\n    }\n\n    public function render()\n    {\n        $this->prepareVars();\n        return $this->makePartial('{{lower_name}}');\n    }\n\n    public function renderForm()\n    {\n        $this->prepareVars();\n        return $this->makePartial('{{lower_name}}_form');\n    }\n\n    public function prepareVars()\n    {\n        $this->vars['scope'] = $this->filterScope;\n        $this->vars['name'] = $this->getScopeName();\n        $this->vars['value'] = $this->getLoadValue();\n    }\n\n    public function loadAssets()\n    {\n        $this->addCss('css/{{lower_name}}.css');\n        $this->addJs('js/{{lower_name}}.js');\n    }\n\n    public function getActiveValue()\n    {\n        if (post('clearScope')) {\n            return null;\n        }\n\n        if (!$this->hasPostValue('value')) {\n            return null;\n        }\n\n        return post('Filter');\n    }\n\n    public function applyScopeToQuery($query)\n    {\n        $hasDiscount = $this->filterScope->value;\n\n        if ($hasDiscount) {\n            $query->where('discount', '>', 0);\n        }\n        else {\n            $query->where('discount', 0);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/filterwidget/javascript.stub",
    "content": "/*\n * This is a sample JavaScript file used by {{name}}\n *\n * You can delete this file if you want\n */\n"
  },
  {
    "path": "src/Scaffold/Console/filterwidget/partial.stub",
    "content": "<a\n    href=\"javascript:;\"\n    class=\"filter-scope <?= $value ? 'active' : '' ?>\"\n    data-scope-name=\"<?= $name ?>\"\n>\n    <span class=\"filter-label\"><?= e(trans($scope->label)) ?></span>\n    <?php if ($value): ?>\n        <span class=\"filter-setting\">1</span>\n    <?php endif ?>\n</a>\n"
  },
  {
    "path": "src/Scaffold/Console/filterwidget/partial_form.stub",
    "content": "<div class=\"filter-box\">\n    <div class=\"filter-facet\">\n        <div class=\"facet-item is-grow\">\n            <select name=\"Filter[value]\" class=\"form-control form-control-sm custom-select\">\n                <option value=\"1\" <?= $scope->value === '1' ? 'selected=\"selected\"' : '' ?>>has a discount</option>\n                <option value=\"0\" <?= $scope->value === '0' ? 'selected=\"selected\"' : '' ?>>does not have a discount</option>\n            </select>\n        </div>\n    </div>\n    <div class=\"filter-buttons\">\n        <button class=\"btn btn-sm btn-primary\" data-filter-action=\"apply\">\n            <?= __(\"Apply\") ?>\n        </button>\n        <div class=\"flex-grow-1\"></div>\n        <button class=\"btn btn-sm btn-secondary\" data-filter-action=\"clear\">\n            <?= __(\"Clear\") ?>\n        </button>\n    </div>\n</div>\n"
  },
  {
    "path": "src/Scaffold/Console/filterwidget/stylesheet.stub",
    "content": "/*\n * This is a sample StyleSheet file used by {{name}}\n *\n * You can delete this file if you want\n */\n"
  },
  {
    "path": "src/Scaffold/Console/formwidget/formwidget.stub",
    "content": "<?php namespace {{namespace_php}}\\FormWidgets;\n\nuse Backend\\Classes\\FormWidgetBase;\n\n/**\n * {{studly_name}} Form Widget\n *\n * @link https://docs.octobercms.com/4.x/extend/forms/form-widgets.html\n */\nclass {{studly_name}} extends FormWidgetBase\n{\n    protected $defaultAlias = '{{lower_plugin}}_{{snake_name}}';\n\n    public function init()\n    {\n    }\n\n    public function render()\n    {\n        $this->prepareVars();\n        return $this->makePartial('{{lower_name}}');\n    }\n\n    public function prepareVars()\n    {\n        $this->vars['name'] = $this->formField->getName();\n        $this->vars['value'] = $this->getLoadValue();\n        $this->vars['model'] = $this->model;\n    }\n\n    public function loadAssets()\n    {\n        $this->addCss('css/{{lower_name}}.css');\n        $this->addJs('js/{{lower_name}}.js');\n    }\n\n    public function getSaveValue($value)\n    {\n        return $value;\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/formwidget/javascript.stub",
    "content": "/*\n * This is a sample JavaScript file used by {{name}}\n *\n * You can delete this file if you want\n */\n"
  },
  {
    "path": "src/Scaffold/Console/formwidget/partial.stub",
    "content": "<?php if ($this->previewMode): ?>\n\n    <div class=\"form-control\">\n        <?= $value ?>\n    </div>\n\n<?php else: ?>\n\n    <input\n        type=\"text\"\n        id=\"<?= $this->getId('input') ?>\"\n        name=\"<?= $name ?>\"\n        value=\"<?= $value ?>\"\n        class=\"form-control\"\n        autocomplete=\"off\" />\n\n<?php endif ?>\n"
  },
  {
    "path": "src/Scaffold/Console/formwidget/stylesheet.stub",
    "content": "/*\n * This is a sample StyleSheet file used by {{name}}\n *\n * You can delete this file if you want\n */\n"
  },
  {
    "path": "src/Scaffold/Console/job/job.queued.stub",
    "content": "<?php namespace {{namespace_php}}\\Jobs;\n\nuse Illuminate\\Bus\\Queueable;\nuse Illuminate\\Contracts\\Queue\\ShouldQueue;\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\nuse Illuminate\\Queue\\InteractsWithQueue;\nuse Illuminate\\Queue\\SerializesModels;\n\n/**\n * {{studly_name}} Job\n */\nclass {{studly_name}} implements ShouldQueue\n{\n    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;\n\n    /**\n     * __construct a new job instance.\n     */\n    public function __construct()\n    {\n        //\n    }\n\n    /**\n     * handle the job.\n     */\n    public function handle(): void\n    {\n        //\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/job/job.stub",
    "content": "<?php namespace {{namespace_php}}\\Jobs;\n\nuse Illuminate\\Foundation\\Bus\\Dispatchable;\n\n/**\n * {{studly_name}} Job\n */\nclass {{studly_name}}\n{\n    use Dispatchable;\n\n    /**\n     * __construct a new job instance.\n     */\n    public function __construct()\n    {\n        //\n    }\n\n    /**\n     * handle the job.\n     */\n    public function handle(): void\n    {\n        //\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/migration/create_app_table.stub",
    "content": "<?php\n\nuse October\\Rain\\Database\\Schema\\Blueprint;\nuse October\\Rain\\Database\\Updates\\Migration;\n\n/**\n * {{studly_name}} Migration\n *\n * @link https://docs.octobercms.com/4.x/extend/database/structure.html\n */\nreturn new class extends Migration\n{\n    /**\n     * up builds the migration\n     */\n    public function up()\n    {\n        Schema::create('{{table}}', function(Blueprint $table) {\n            $table->id();\n{% if timestamps %}\n            $table->timestamps();\n{% endif %}{% if softDeletes %}\n            $table->softDeletes();\n{% endif %}\n        });\n    }\n\n    /**\n     * down reverses the migration\n     */\n    public function down()\n    {\n        Schema::dropIfExists('{{table}}');\n    }\n};\n"
  },
  {
    "path": "src/Scaffold/Console/migration/create_table.stub",
    "content": "<?php namespace {{namespace_php}}\\Updates;\n\nuse Schema;\nuse October\\Rain\\Database\\Schema\\Blueprint;\nuse October\\Rain\\Database\\Updates\\Migration;\n\n/**\n * {{studly_name}} Migration\n *\n * @link https://docs.octobercms.com/4.x/extend/database/structure.html\n */\nreturn new class extends Migration\n{\n    /**\n     * up builds the migration\n     */\n    public function up()\n    {\n        Schema::create('{{table}}', function(Blueprint $table) {\n            $table->id();\n{% if timestamps %}\n            $table->timestamps();\n{% endif %}{% if softDeletes %}\n            $table->softDeletes();\n{% endif %}\n        });\n    }\n\n    /**\n     * down reverses the migration\n     */\n    public function down()\n    {\n        Schema::dropIfExists('{{table}}');\n    }\n};\n"
  },
  {
    "path": "src/Scaffold/Console/migration/update_app_table.stub",
    "content": "<?php\n\nuse October\\Rain\\Database\\Schema\\Blueprint;\nuse October\\Rain\\Database\\Updates\\Migration;\n\n/**\n * {{studly_name}} Migration\n *\n * @link https://docs.octobercms.com/4.x/extend/database/structure.html\n */\nreturn new class extends Migration\n{\n    /**\n     * up builds the migration\n     */\n    public function up()\n    {\n        Schema::table('{{table}}', function(Blueprint $table) {\n            // ...\n        });\n    }\n\n    /**\n     * down reverses the migration\n     */\n    public function down()\n    {\n        Schema::table('{{table}}', function(Blueprint $table) {\n            // ...\n        });\n    }\n};\n"
  },
  {
    "path": "src/Scaffold/Console/migration/update_table.stub",
    "content": "<?php namespace {{namespace_php}}\\Updates;\n\nuse Schema;\nuse October\\Rain\\Database\\Schema\\Blueprint;\nuse October\\Rain\\Database\\Updates\\Migration;\n\n/**\n * {{studly_name}} Migration\n *\n * @link https://docs.octobercms.com/4.x/extend/database/structure.html\n */\nreturn new class extends Migration\n{\n    /**\n     * up builds the migration\n     */\n    public function up()\n    {\n        Schema::table('{{table}}', function(Blueprint $table) {\n            // ...\n        });\n    }\n\n    /**\n     * down reverses the migration\n     */\n    public function down()\n    {\n        Schema::table('{{table}}', function(Blueprint $table) {\n            // ...\n        });\n    }\n};\n"
  },
  {
    "path": "src/Scaffold/Console/model/columns.stub",
    "content": "# ===================================\n#  List Column Definitions\n# ===================================\n\ncolumns:\n    id:\n        label: ID\n        searchable: true\n"
  },
  {
    "path": "src/Scaffold/Console/model/fields.stub",
    "content": "# ===================================\n#  Form Field Definitions\n# ===================================\n\nfields:\n    id:\n        label: ID\n        disabled: true\n"
  },
  {
    "path": "src/Scaffold/Console/model/model.stub",
    "content": "<?php namespace {{namespace_php}}\\Models;\n\nuse Model;\n\n/**\n * {{studly_name}} Model\n *\n * @link https://docs.octobercms.com/4.x/extend/system/models.html\n */\nclass {{studly_name}} extends Model\n{\n    use \\October\\Rain\\Database\\Traits\\Validation;\n{% if softDeletes %}\n    use \\October\\Rain\\Database\\Traits\\SoftDelete;\n{% endif %}\n\n    /**\n     * @var string table name\n     */\n    public $table = '{{namespace_table}}_{{snake_plural_name}}';\n\n    /**\n     * @var array rules for validation\n     */\n    public $rules = [];\n{% if softDeletes %}\n\n    /**\n     * @var array dates used by the model\n     */\n    protected $dates = [\n        'deleted_at'\n    ];\n{% endif %}{% if not timestamps %}\n\n    /**\n     * @var bool timestamps disabled by default\n     */\n    public $timestamps = false;\n{% endif %}\n}\n"
  },
  {
    "path": "src/Scaffold/Console/plugin/composer.stub",
    "content": "{\n    \"name\": \"{{lower_author}}/{{lower_plugin}}-plugin\",\n    \"type\": \"october-plugin\",\n    \"description\": \"No description provided yet...\",\n    \"require\": {\n        \"composer/installers\": \"~1.0\"\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/plugin/plugin.stub",
    "content": "<?php namespace {{studly_author}}\\{{studly_plugin}};\n\nuse Backend;\nuse System\\Classes\\PluginBase;\n\n/**\n * Plugin Information File\n *\n * @link https://docs.octobercms.com/4.x/extend/system/plugins.html\n */\nclass Plugin extends PluginBase\n{\n    /**\n     * pluginDetails about this plugin.\n     */\n    public function pluginDetails()\n    {\n        return [\n            'name' => '{{plugin}}',\n            'description' => 'No description provided yet...',\n            'author' => '{{author}}',\n            'icon' => 'icon-leaf'\n        ];\n    }\n\n    /**\n     * register method, called when the plugin is first registered.\n     */\n    public function register()\n    {\n        //\n    }\n\n    /**\n     * boot method, called right before the request route.\n     */\n    public function boot()\n    {\n        //\n    }\n\n    /**\n     * registerComponents used by the frontend.\n     */\n    public function registerComponents()\n    {\n        return []; // Remove this line to activate\n\n        return [\n            '{{studly_author}}\\{{studly_plugin}}\\Components\\MyComponent' => 'myComponent',\n        ];\n    }\n\n    /**\n     * registerPermissions used by the backend.\n     */\n    public function registerPermissions()\n    {\n        return []; // Remove this line to activate\n\n        return [\n            '{{lower_namespace_code}}.some_permission' => [\n                'tab' => '{{plugin}}',\n                'label' => 'Some permission'\n            ],\n        ];\n    }\n\n    /**\n     * registerNavigation used by the backend.\n     */\n    public function registerNavigation()\n    {\n        return []; // Remove this line to activate\n\n        return [\n            '{{lower_plugin}}' => [\n                'label' => '{{plugin}}',\n                'url' => Backend::url('{{lower_author}}/{{lower_plugin}}/mycontroller'),\n                'icon' => 'icon-leaf',\n                'permissions' => ['{{lower_namespace_code}}.*'],\n                'order' => 500,\n            ],\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/plugin/version.stub",
    "content": "v1.0.1: First version of {{plugin}}\n"
  },
  {
    "path": "src/Scaffold/Console/reportwidget/reportwidget.stub",
    "content": "<?php namespace {{namespace_php}}\\ReportWidgets;\n\nuse Dashboard\\Classes\\ReportWidgetBase;\nuse Exception;\n\n/**\n * {{studly_name}} Report Widget\n *\n * @link https://docs.octobercms.com/4.x/extend/dashboards/report-widgets.html\n */\nclass {{studly_name}} extends ReportWidgetBase\n{\n    protected $defaultAlias = '{{studly_name}}ReportWidget';\n\n    public function defineProperties()\n    {\n        return [\n            'name' => [\n                'title' => 'Name',\n                'default' => '{{title_name}} Report Widget',\n                'type' => 'string',\n                'validation' => [\n                    'required' => [\n                        'message' => 'The Name field is required'\n                    ],\n                    'regex' => [\n                        'message' => 'The Name field can contain only Latin letters.',\n                        'pattern' => '^[a-zA-Z]+$'\n                    ]\n                ]\n            ],\n        ];\n    }\n\n    public function render()\n    {\n        try {\n            $this->prepareVars();\n        }\n        catch (Exception $ex) {\n            $this->vars['error'] = $ex->getMessage();\n        }\n\n        return $this->makePartial('{{lower_name}}');\n    }\n\n    public function prepareVars()\n    {\n    }\n\n    protected function loadAssets()\n    {\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/reportwidget/widget.stub",
    "content": "<div class=\"report-widget\">\n    <h3><?= e($this->property('title')) ?></h3>\n\n    <?php if (!isset($error)): ?>\n        <p>This is the default partial content.</p>\n    <?php else: ?>\n        <p class=\"flash-message static warning\"><?= e($error) ?></p>\n    <?php endif ?>\n</div>\n"
  },
  {
    "path": "src/Scaffold/Console/seeder/create_app_seeder.stub",
    "content": "<?php namespace {{namespace_php}}\\Database\\Seeders;\n\nuse Seeder;\n\n/**\n * {{studly_name}}\n */\nclass {{studly_name}} extends Seeder\n{\n    /**\n     * run the database seeds.\n     */\n    public function run()\n    {\n        //\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/seeder/create_seeder.stub",
    "content": "<?php namespace {{namespace_php}}\\Updates\\Seeders;\n\nuse Seeder;\n\n/**\n * {{studly_name}}\n */\nclass {{studly_name}} extends Seeder\n{\n    /**\n     * run the database seeds.\n     */\n    public function run()\n    {\n        //\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/Console/test/phpunit.app.stub",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit backupGlobals=\"false\"\n    backupStaticAttributes=\"false\"\n    bootstrap=\"../modules/system/tests/bootstrap.php\"\n    colors=\"true\"\n    convertErrorsToExceptions=\"true\"\n    convertNoticesToExceptions=\"true\"\n    convertWarningsToExceptions=\"true\"\n    processIsolation=\"false\"\n    stopOnFailure=\"false\"\n>\n    <testsuites>\n        <testsuite name=\"App Test Suite\">\n            <directory>./tests</directory>\n            <exclude>./tests/browser</exclude>\n        </testsuite>\n    </testsuites>\n    <php>\n        <env name=\"APP_ENV\" value=\"testing\" />\n        <env name=\"CACHE_DRIVER\" value=\"array\" />\n        <env name=\"SESSION_DRIVER\" value=\"array\" />\n        <env name=\"ACTIVE_THEME\" value=\"test\" />\n        <env name=\"CONVERT_LINE_ENDINGS\" value=\"true\" />\n        <env name=\"CMS_ROUTE_CACHE\" value=\"true\" />\n        <env name=\"CMS_TWIG_CACHE\" value=\"false\" />\n        <env name=\"ENABLE_CSRF\" value=\"false\" />\n        <env name=\"DB_CONNECTION\" value=\"sqlite\" />\n        <env name=\"DB_DATABASE\" value=\":memory:\" />\n    </php>\n</phpunit>\n"
  },
  {
    "path": "src/Scaffold/Console/test/phpunit.plugin.stub",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit backupGlobals=\"false\"\n    backupStaticAttributes=\"false\"\n    bootstrap=\"../../../modules/system/tests/bootstrap.php\"\n    colors=\"true\"\n    convertErrorsToExceptions=\"true\"\n    convertNoticesToExceptions=\"true\"\n    convertWarningsToExceptions=\"true\"\n    processIsolation=\"false\"\n    stopOnFailure=\"false\"\n>\n    <testsuites>\n        <testsuite name=\"Plugin Test Suite\">\n            <directory>./tests</directory>\n            <exclude>./tests/browser</exclude>\n        </testsuite>\n    </testsuites>\n    <php>\n        <env name=\"APP_ENV\" value=\"testing\" />\n        <env name=\"CACHE_DRIVER\" value=\"array\" />\n        <env name=\"SESSION_DRIVER\" value=\"array\" />\n        <env name=\"ACTIVE_THEME\" value=\"test\" />\n        <env name=\"CONVERT_LINE_ENDINGS\" value=\"true\" />\n        <env name=\"CMS_ROUTE_CACHE\" value=\"true\" />\n        <env name=\"CMS_TWIG_CACHE\" value=\"false\" />\n        <env name=\"ENABLE_CSRF\" value=\"false\" />\n        <env name=\"DB_CONNECTION\" value=\"sqlite\" />\n        <env name=\"DB_DATABASE\" value=\":memory:\" />\n    </php>\n</phpunit>\n"
  },
  {
    "path": "src/Scaffold/Console/test/test.stub",
    "content": "<?php namespace {{namespace_php}}\\Tests;\n\nuse PluginTestCase;\n\n/**\n * {{studly_name}} Test\n */\nclass {{studly_name}} extends PluginTestCase\n{\n    /**\n     * test_example is a basic test\n     */\n    public function test_example()\n    {\n        $this->assertTrue(true);\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/GeneratorCommand.php",
    "content": "<?php namespace October\\Rain\\Scaffold;\n\nuse ReflectionClass;\nuse October\\Rain\\Support\\Str;\nuse Illuminate\\Console\\Command;\nuse October\\Rain\\Filesystem\\Filesystem;\nuse Symfony\\Component\\Console\\Input\\InputArgument;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Exception;\nuse Twig;\n\n/**\n * GeneratorCommand\n * @deprecated use GeneratorCommandBase\n */\nabstract class GeneratorCommand extends Command\n{\n    /**\n     * @var \\October\\Rain\\Filesystem\\Filesystem files is the filesystem instance\n     */\n    protected $files;\n\n    /**\n     * @var string type of class being generated\n     */\n    protected $type;\n\n    /**\n     * @var array stubs is a mapping of stub to generated file\n     */\n    protected $stubs = [];\n\n    /**\n     * @var array vars to use in stubs\n     */\n    protected $vars = [];\n\n    /**\n     * __construct creates a new controller creator command instance\n     */\n    public function __construct()\n    {\n        parent::__construct();\n\n        $this->files = new Filesystem;\n    }\n\n    /**\n     * handle executes the console command\n     */\n    public function handle()\n    {\n        $this->vars = $this->processVars($this->prepareVars());\n\n        $this->makeStubs();\n\n        $this->info($this->type . ' created successfully.');\n    }\n\n    /**\n     * prepareVars prepares variables for stubs\n     */\n    abstract protected function prepareVars();\n\n    /**\n     * makeStubs makes all stubs\n     */\n    public function makeStubs()\n    {\n        $stubs = array_keys($this->stubs);\n\n        foreach ($stubs as $stub) {\n            $this->makeStub($stub);\n        }\n    }\n\n    /**\n     * makeStub makes a single stub\n     */\n    public function makeStub(string $stubName)\n    {\n        if (!isset($this->stubs[$stubName])) {\n            return;\n        }\n\n        $sourceFile = $this->getSourcePath() . '/' . $stubName;\n        $destinationFile = $this->getDestinationPath() . '/' . $this->stubs[$stubName];\n        $destinationContent = $this->files->get($sourceFile);\n\n        // Parse each variable in to the destination content and path\n        $destinationContent = Twig::parse($destinationContent, $this->vars);\n        $destinationFile = Twig::parse($destinationFile, $this->vars);\n\n        $this->makeDirectory($destinationFile);\n\n        // Make sure this file does not already exist\n        if ($this->files->exists($destinationFile) && !$this->option('force')) {\n            throw new Exception('Stop everything!!! This file already exists: ' . $destinationFile);\n        }\n\n        $this->files->put($destinationFile, $destinationContent);\n    }\n\n    /**\n     * makeDirectory builds the directory for the class if necessary\n     */\n    protected function makeDirectory(string $path)\n    {\n        if (!$this->files->isDirectory(dirname($path))) {\n            $this->files->makeDirectory(dirname($path), 0755, true, true);\n        }\n    }\n\n    /**\n     * processVars converts all variables to available modifier and case formats\n     * Syntax is CASE_MODIFIER_KEY, eg: lower_plural_xxx\n     */\n    protected function processVars(array $vars): array\n    {\n        $cases = ['upper', 'lower', 'snake', 'studly', 'camel', 'title'];\n        $modifiers = ['plural', 'singular', 'title'];\n\n        foreach ($vars as $key => $var) {\n            // Apply cases, and cases with modifiers\n            foreach ($cases as $case) {\n                $primaryKey = $case . '_' . $key;\n                $vars[$primaryKey] = $this->modifyString($case, $var);\n\n                foreach ($modifiers as $modifier) {\n                    $secondaryKey = $case . '_' . $modifier . '_' . $key;\n                    $vars[$secondaryKey] = $this->modifyString([$modifier, $case], $var);\n                }\n            }\n\n            // Apply modifiers\n            foreach ($modifiers as $modifier) {\n                $primaryKey = $modifier . '_' . $key;\n                $vars[$primaryKey] = $this->modifyString($modifier, $var);\n            }\n        }\n\n        return $vars;\n    }\n\n    /**\n     * modifyString is an internal helper that handles modify a string, with extra logic\n     */\n    protected function modifyString($type, string $string): string\n    {\n        if (is_array($type)) {\n            foreach ($type as $_type) {\n                $string = $this->modifyString($_type, $string);\n            }\n\n            return $string;\n        }\n\n        if ($type === 'title') {\n            $string = str_replace('_', ' ', Str::snake($string));\n        }\n\n        return Str::$type($string);\n    }\n\n    /**\n     * getDestinationPath gets the plugin path from the input\n     */\n    protected function getDestinationPath(): string\n    {\n        $plugin = $this->getPluginInput();\n\n        $parts = explode('.', $plugin);\n        $name = array_pop($parts);\n        $author = array_pop($parts);\n\n        return plugins_path(strtolower($author) . '/' . strtolower($name));\n    }\n\n    /**\n     * getSourcePath gets the source file path\n     */\n    protected function getSourcePath(): string\n    {\n        $className = get_class($this);\n        $class = new ReflectionClass($className);\n\n        return dirname($class->getFileName());\n    }\n\n    /**\n     * getPluginInput gets the desired plugin name from the input\n     */\n    protected function getPluginInput(): string\n    {\n        return $this->argument('plugin');\n    }\n\n    /**\n     * getArguments get the console command arguments\n     */\n    protected function getArguments()\n    {\n        return [\n            ['plugin', InputArgument::REQUIRED, 'The name of the plugin to create. Eg: RainLab.Blog'],\n        ];\n    }\n\n    /**\n     * getOptions get the console command options\n     */\n    protected function getOptions()\n    {\n        return [\n            ['force', null, InputOption::VALUE_NONE, 'Overwrite existing files with generated ones.'],\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/GeneratorCommandBase.php",
    "content": "<?php namespace October\\Rain\\Scaffold;\n\nuse Twig;\nuse October\\Rain\\Support\\Str;\nuse Illuminate\\Console\\Command;\nuse October\\Rain\\Filesystem\\Filesystem;\nuse ReflectionClass;\nuse Exception;\n\n/**\n * GeneratorCommandBase base class\n */\nabstract class GeneratorCommandBase extends Command\n{\n    /**\n     * @var \\October\\Rain\\Filesystem\\Filesystem files is the filesystem instance\n     */\n    protected $files;\n\n    /**\n     * @var string typeLabel of class being generated\n     */\n    protected $typeLabel;\n\n    /**\n     * @var array vars to use in stubs\n     */\n    protected $vars = [];\n\n    /**\n     * __construct creates a new controller creator command instance\n     */\n    public function __construct()\n    {\n        parent::__construct();\n\n        $this->files = new Filesystem;\n    }\n\n    /**\n     * handle executes the console command\n     */\n    public function handle()\n    {\n        $this->vars = $this->processVars($this->prepareVars());\n\n        $this->makeStubs();\n\n        $this->components->info(\"{$this->typeLabel} created successfully.\");\n    }\n\n    /**\n     * prepareVars prepares variables for stubs\n     */\n    abstract protected function prepareVars();\n\n    /**\n     * makeStubs makes all stubs\n     */\n    abstract public function makeStubs();\n\n    /**\n     * makeStub makes a single stub\n     */\n    public function makeStub(string $stubName, string $outputName)\n    {\n        $sourceFile = $this->getSourcePath() . '/' . $stubName;\n        $destinationFile = $this->getDestinationPath() . '/' . $outputName;\n        $destinationContent = $this->files->get($sourceFile);\n\n        // Parse each variable in to the destination content and path\n        $destinationContent = Twig::parse($destinationContent, $this->vars);\n        $destinationFile = Twig::parse($destinationFile, $this->vars);\n\n        $this->makeDirectory($destinationFile);\n\n        // Make sure this file does not already exist\n        if ($this->files->exists($destinationFile) && !$this->option('overwrite')) {\n            throw new Exception('Process halted! This file already exists: ' . $destinationFile);\n        }\n\n        $this->files->put($destinationFile, $destinationContent);\n    }\n\n    /**\n     * makeDirectory builds the directory for the class if necessary\n     */\n    protected function makeDirectory(string $path)\n    {\n        if (!$this->files->isDirectory(dirname($path))) {\n            $this->files->makeDirectory(dirname($path), 0755, true, true);\n        }\n    }\n\n    /**\n     * processVars converts all variables to available modifier and case formats\n     * Syntax is CASE_MODIFIER_KEY, eg: lower_plural_xxx\n     */\n    protected function processVars(array $vars): array\n    {\n        $cases = ['upper', 'lower', 'snake', 'studly', 'camel', 'title'];\n        $modifiers = ['plural', 'singular', 'title'];\n\n        // Namespace modifiers\n        if (isset($vars['namespace'])) {\n            $vars += $this->getNamespaceModifiers();\n        }\n\n        // Splice in author and plugin name automatically\n        [$author, $plugin] = $this->getFormattedNamespace();\n        $vars += [\n            'author' => $author,\n            'plugin' => $plugin,\n        ];\n\n        // Process variables\n        foreach ($vars as $key => $var) {\n            // Apply cases, and cases with modifiers\n            foreach ($cases as $case) {\n                $primaryKey = $case . '_' . $key;\n                $vars[$primaryKey] = $this->modifyString($case, $var);\n\n                foreach ($modifiers as $modifier) {\n                    $secondaryKey = $case . '_' . $modifier . '_' . $key;\n                    $vars[$secondaryKey] = $this->modifyString([$modifier, $case], $var);\n                }\n            }\n\n            // Apply modifiers\n            foreach ($modifiers as $modifier) {\n                $primaryKey = $modifier . '_' . $key;\n                $vars[$primaryKey] = $this->modifyString($modifier, $var);\n            }\n        }\n\n        return $vars;\n    }\n\n    /**\n     * modifyString is an internal helper that handles modify a string, with extra logic\n     */\n    protected function modifyString($type, string $string): string\n    {\n        if (is_array($type)) {\n            foreach ($type as $_type) {\n                $string = $this->modifyString($_type, $string);\n            }\n\n            return $string;\n        }\n\n        if ($type === 'title') {\n            $string = str_replace('_', ' ', Str::snake($string));\n        }\n\n        return Str::$type($string);\n    }\n\n    /**\n     * getNamespaceModifiers\n     */\n    protected function getNamespaceModifiers(): array\n    {\n        if ($this->isAppNamespace()) {\n            return [\n                'namespace_php' => 'App',\n                'namespace_code' => 'App',\n                'namespace_path' => 'app',\n                'namespace_table' => 'app',\n                'namespace_local' => '~/app',\n            ];\n        }\n\n        [$author, $plugin] = $this->getFormattedNamespace();\n        $sAuthor = Str::studly($author);\n        $sPlugin = Str::studly($plugin);\n        $lAuthor = mb_strtolower($author);\n        $lPlugin = mb_strtolower($plugin);\n\n        return [\n            'namespace_php' => \"{$sAuthor}\\\\{$sPlugin}\",\n            'namespace_code' => \"{$sAuthor}.{$sPlugin}\",\n            'namespace_path' => \"{$lAuthor}/{$lPlugin}\",\n            'namespace_table' => \"{$lAuthor}_{$lPlugin}\",\n            'namespace_local' => \"$/{$lAuthor}/{$lPlugin}\",\n        ];\n    }\n\n    /**\n     * getNamespaceTable produces a table name (e.g. acme_blog)\n     */\n    protected function getNamespaceTable(): string\n    {\n        return $this->getNamespaceModifiers()['namespace_table'];\n    }\n\n    /**\n     * getDestinationPath gets the app or plugin local path\n     */\n    protected function getDestinationPath(): string\n    {\n        if ($this->isAppNamespace()) {\n            return app_path();\n        }\n\n        return plugins_path($this->getNamespaceModifiers()['namespace_path']);\n    }\n\n    /**\n     * getSourcePath gets the source file path\n     */\n    protected function getSourcePath(): string\n    {\n        $class = new ReflectionClass(static::class);\n\n        return dirname($class->getFileName());\n    }\n\n    /**\n     * getFormattedNamespace returns a tuple of author and plugin name, or app,\n     * where returned array takes format of [author, name]\n     */\n    protected function getFormattedNamespace(): array\n    {\n        $namespace = $this->getNamespaceInput();\n\n        if (strpos($namespace, '.') !== false) {\n            $parts = explode('.', $namespace);\n            return [$parts[0], $parts[1]];\n        }\n\n        if (strpos($namespace, '\\\\') !== false) {\n            $parts = explode('\\\\', $namespace);\n            return [$parts[0], $parts[1]];\n        }\n\n        return [$namespace, $namespace];\n    }\n\n    /**\n     * getNamespaceInput gets the desired plugin name from the input\n     */\n    protected function getNamespaceInput(): string\n    {\n        return $this->argument('namespace');\n    }\n\n    /**\n     * isAppNamespace\n     */\n    protected function isAppNamespace(): bool\n    {\n        return mb_strtolower(trim($this->getNamespaceInput())) === 'app';\n    }\n}\n"
  },
  {
    "path": "src/Scaffold/ScaffoldServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Scaffold;\n\nuse October\\Rain\\Scaffold\\Console\\CreateCommand;\nuse October\\Rain\\Scaffold\\Console\\CreateFactory;\nuse October\\Rain\\Scaffold\\Console\\CreatePlugin;\nuse October\\Rain\\Scaffold\\Console\\CreateModel;\nuse October\\Rain\\Scaffold\\Console\\CreateMigration;\nuse October\\Rain\\Scaffold\\Console\\CreateController;\nuse October\\Rain\\Scaffold\\Console\\CreateComponent;\nuse October\\Rain\\Scaffold\\Console\\CreateFormWidget;\nuse October\\Rain\\Scaffold\\Console\\CreateReportWidget;\nuse October\\Rain\\Scaffold\\Console\\CreateFilterWidget;\nuse October\\Rain\\Scaffold\\Console\\CreateContentField;\nuse October\\Rain\\Scaffold\\Console\\CreateSeeder;\nuse October\\Rain\\Scaffold\\Console\\CreateTest;\nuse October\\Rain\\Scaffold\\Console\\CreateJob;\nuse Illuminate\\Contracts\\Support\\DeferrableProvider;\nuse Illuminate\\Support\\ServiceProvider;\n\n/**\n * ScaffoldServiceProvider\n */\nclass ScaffoldServiceProvider extends ServiceProvider implements DeferrableProvider\n{\n    /**\n     * register the service provider.\n     */\n    public function register()\n    {\n        if (!$this->app->runningInConsole()) {\n            return;\n        }\n\n        $this->app->singleton('command.create.plugin', CreatePlugin::class);\n        $this->app->singleton('command.create.model', CreateModel::class);\n        $this->app->singleton('command.create.migration', CreateMigration::class);\n        $this->app->singleton('command.create.controller', CreateController::class);\n        $this->app->singleton('command.create.component', CreateComponent::class);\n        $this->app->singleton('command.create.formwidget', CreateFormWidget::class);\n        $this->app->singleton('command.create.reportwidget', CreateReportWidget::class);\n        $this->app->singleton('command.create.filterwidget', CreateFilterWidget::class);\n        $this->app->singleton('command.create.contentfield', CreateContentField::class);\n        $this->app->singleton('command.create.command', CreateCommand::class);\n        $this->app->singleton('command.create.test', CreateTest::class);\n        $this->app->singleton('command.create.job', CreateJob::class);\n        $this->app->singleton('command.create.factory', CreateFactory::class);\n        $this->app->singleton('command.create.seeder', CreateSeeder::class);\n\n        $this->commands('command.create.plugin');\n        $this->commands('command.create.model');\n        $this->commands('command.create.migration');\n        $this->commands('command.create.controller');\n        $this->commands('command.create.component');\n        $this->commands('command.create.formwidget');\n        $this->commands('command.create.reportwidget');\n        $this->commands('command.create.filterwidget');\n        $this->commands('command.create.contentfield');\n        $this->commands('command.create.command');\n        $this->commands('command.create.test');\n        $this->commands('command.create.job');\n        $this->commands('command.create.factory');\n        $this->commands('command.create.seeder');\n    }\n\n    /**\n     * provides the returned services.\n     * @return array\n     */\n    public function provides()\n    {\n        return [\n            'command.create.plugin',\n            'command.create.model',\n            'command.create.migration',\n            'command.create.controller',\n            'command.create.component',\n            'command.create.formwidget',\n            'command.create.reportwidget',\n            'command.create.filterwidget',\n            'command.create.contentfield',\n            'command.create.command',\n            'command.create.test',\n            'command.create.job',\n            'command.create.factory',\n            'command.create.seeder',\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Support/Arr.php",
    "content": "<?php namespace October\\Rain\\Support;\n\nuse Lang;\nuse Illuminate\\Support\\Arr as ArrHelper;\n\n/**\n * Arr helper as an extension to Laravel\n *\n * @package october\\support\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Arr extends ArrHelper\n{\n    /**\n     * build a new array using a callback.\n     */\n    public static function build($array, callable $callback): array\n    {\n        $results = [];\n\n        foreach ($array as $key => $value) {\n            list($innerKey, $innerValue) = call_user_func($callback, $key, $value);\n\n            $results[$innerKey] = $innerValue;\n        }\n\n        return $results;\n    }\n\n    /**\n     * trans will translate an array, usually for dropdown and checkboxlist options\n     */\n    public static function trans(array $arr): array\n    {\n        array_walk_recursive($arr, function(&$value, $key) {\n            if (is_string($value)) {\n                $value = Lang::get($value);\n            }\n        });\n\n        return $arr;\n    }\n}\n"
  },
  {
    "path": "src/Support/Collection.php",
    "content": "<?php namespace October\\Rain\\Support;\n\nuse Illuminate\\Support\\Collection as CollectionBase;\n\n/**\n * Collection is an umbrella class for Laravel's Collection\n *\n * @package october\\support\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Collection extends CollectionBase\n{\n    /**\n     * lists get an array with the values of a given key\n     * @param  string  $value\n     * @param  string  $key\n     * @return array\n     */\n    public function lists($value, $key = null)\n    {\n        return $this->pluck($value, $key)->all();\n    }\n}\n"
  },
  {
    "path": "src/Support/Date.php",
    "content": "<?php namespace October\\Rain\\Support;\n\nuse Carbon\\Carbon as DateBase;\n\n/**\n * Date is an umbrella class for Carbon that automatically applies localizations\n *\n * @package october\\support\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Date extends DateBase\n{\n    /**\n     * format\n     */\n    public function format(string $format): string\n    {\n        return parent::translatedFormat($format);\n    }\n\n    /**\n     * createFromFormat\n     */\n    public static function createFromFormat($format, $time, $timezone = null): ?static\n    {\n        if (is_string($time)) {\n            $time = static::translateTimeString($time, static::getLocale(), 'en');\n        }\n\n        return parent::rawCreateFromFormat($format, $time, $timezone);\n    }\n\n    /**\n     * parse\n     */\n    public static function parse($time = null, $timezone = null): static\n    {\n        if (is_string($time)) {\n            $time = static::translateTimeString($time, static::getLocale(), 'en');\n        }\n\n        return parent::rawParse($time, $timezone);\n    }\n}\n"
  },
  {
    "path": "src/Support/Debug/HtmlDumper.php",
    "content": "<?php namespace October\\Rain\\Support\\Debug;\n\nuse Symfony\\Component\\VarDumper\\Dumper\\HtmlDumper as SymfonyHtmlDumper;\n\n/**\n * HtmlDumper is used for debugging Twig\n */\nclass HtmlDumper extends SymfonyHtmlDumper\n{\n    /**\n     * @var array styles definition for output\n     */\n    protected array $styles = [\n        'default' => 'background-color:#fff; color:#222; line-height:1.2em; font-weight:normal; font:12px Monaco, Consolas, monospace; word-wrap: break-word; white-space: pre-wrap; position:relative; z-index:100000',\n        'num' => 'color:#a71d5d',\n        'const' => 'color:#795da3',\n        'str' => 'color:#df5000',\n        'cchr' => 'color:#222',\n        'note' => 'color:#a71d5d',\n        'ref' => 'color:#a0a0a0',\n        'public' => 'color:#795da3',\n        'protected' => 'color:#795da3',\n        'private' => 'color:#795da3',\n        'meta' => 'color:#b729d9',\n        'key' => 'color:#df5000',\n        'index' => 'color:#a71d5d',\n    ];\n}\n"
  },
  {
    "path": "src/Support/DefaultProviders.php",
    "content": "<?php\n\nnamespace October\\Rain\\Support;\n\nuse Illuminate\\Support\\DefaultProviders as DefaultProvidersBase;\n\nclass DefaultProviders extends DefaultProvidersBase\n{\n    /**\n     * Create a new default provider collection.\n     *\n     * @return void\n     */\n    public function __construct(?array $providers = null)\n    {\n        $this->providers = $providers ?: [\n            // October Providers\n            //\n            \\October\\Rain\\Foundation\\Providers\\AppServiceProvider::class,\n            \\October\\Rain\\Foundation\\Providers\\DateServiceProvider::class,\n            \\October\\Rain\\Database\\DatabaseServiceProvider::class,\n            \\October\\Rain\\Halcyon\\HalcyonServiceProvider::class,\n            \\October\\Rain\\Filesystem\\FilesystemServiceProvider::class,\n            \\October\\Rain\\Html\\UrlServiceProvider::class,\n\n            // October Providers (Deferred)\n            \\October\\Rain\\Mail\\MailServiceProvider::class,\n            \\October\\Rain\\Html\\HtmlServiceProvider::class,\n            \\October\\Rain\\Flash\\FlashServiceProvider::class,\n            \\October\\Rain\\Parse\\ParseServiceProvider::class,\n            \\October\\Rain\\Assetic\\AsseticServiceProvider::class,\n            \\October\\Rain\\Resize\\ResizeServiceProvider::class,\n            \\October\\Rain\\Validation\\ValidationServiceProvider::class,\n            \\October\\Rain\\Translation\\TranslationServiceProvider::class,\n            \\Illuminate\\Auth\\Passwords\\PasswordResetServiceProvider:: class,\n\n            // October Console (Deferred)\n            \\October\\Rain\\Scaffold\\ScaffoldServiceProvider::class,\n            \\October\\Rain\\Foundation\\Providers\\ConsoleSupportServiceProvider::class,\n\n            // Laravel Providers\n            //\n            \\Illuminate\\Broadcasting\\BroadcastServiceProvider::class,\n            \\Illuminate\\Bus\\BusServiceProvider::class,\n            \\Illuminate\\Cache\\CacheServiceProvider::class,\n            \\Illuminate\\Concurrency\\ConcurrencyServiceProvider::class,\n            \\Illuminate\\Cookie\\CookieServiceProvider::class,\n            \\Illuminate\\Encryption\\EncryptionServiceProvider::class,\n            \\Illuminate\\Foundation\\Providers\\FoundationServiceProvider::class,\n            \\Illuminate\\Hashing\\HashServiceProvider::class,\n            \\Illuminate\\Pagination\\PaginationServiceProvider::class,\n            \\Illuminate\\Pipeline\\PipelineServiceProvider::class,\n            \\Illuminate\\Queue\\QueueServiceProvider::class,\n            \\Illuminate\\Redis\\RedisServiceProvider::class,\n            \\Illuminate\\Session\\SessionServiceProvider::class,\n            \\Illuminate\\View\\ViewServiceProvider::class,\n        ];\n    }\n}\n"
  },
  {
    "path": "src/Support/Facade.php",
    "content": "<?php namespace October\\Rain\\Support;\n\nuse Illuminate\\Support\\Facades\\Facade as FacadeParent;\n\n/**\n * Facade base class that adds the ability to define a fallback instance\n *\n * @package october\\support\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Facade extends FacadeParent\n{\n    /**\n     * @inheritDoc\n     */\n    protected static function resolveFacadeInstance($name)\n    {\n        if (\n            !is_object($name) &&\n            !is_null(static::$app) &&\n            !static::$app->bound($name) &&\n            ($instance = static::getFacadeInstance()) !== null\n        ) {\n            static::$app->instance($name, $instance);\n        }\n\n        return parent::resolveFacadeInstance($name);\n    }\n\n    /**\n     * getFacadeInstance if the accessor is not found via getFacadeAccessor,\n     * use this instance as a fallback.\n     * @return mixed\n     */\n    protected static function getFacadeInstance()\n    {\n        return null;\n    }\n\n    /**\n     * defaultAliases gets the application default aliases.\n     * @return \\Illuminate\\Support\\Collection\n     */\n    public static function defaultAliases()\n    {\n        return parent::defaultAliases()->merge([\n            'Model' => \\October\\Rain\\Database\\Model::class,\n            'Event' => \\October\\Rain\\Support\\Facades\\Event::class,\n            'Mail' => \\October\\Rain\\Support\\Facades\\Mail::class,\n            'File' => \\October\\Rain\\Support\\Facades\\File::class,\n            'Config' => \\October\\Rain\\Support\\Facades\\Config::class,\n            'Seeder' => \\October\\Rain\\Database\\Updates\\Seeder::class,\n            'Input' => \\October\\Rain\\Support\\Facades\\Input::class,\n            'Str' => \\October\\Rain\\Support\\Facades\\Str::class,\n        ]);\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Auth.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse Illuminate\\Support\\Facades\\Auth as AuthBase;\n\n/**\n * Auth\n *\n * @see \\RainLab\\User\\Classes\\AuthManager\n */\nclass Auth extends AuthBase\n{\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'auth';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Block.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse October\\Rain\\Support\\Facade;\n\n/**\n * Block\n *\n * @method static void put(string $name)\n * @method static void startBlock(string $name)\n * @method static void endPut(bool $append = false)\n * @method static void endBlock(bool $append = false)\n * @method static void set(string $name, string $content)\n * @method static void append(string $name, string $content)\n * @method static string placeholder(string $name, string $default = null)\n * @method static string get(string $name, string $default = null)\n * @method static void reset()\n *\n * @see \\October\\Rain\\Html\\BlockBuilder\n */\nclass Block extends Facade\n{\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'block';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Config.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse October\\Rain\\Support\\Facade;\n\n/**\n * Config\n *\n * @method static bool has(string $key)\n * @method static bool hasGroup(string $key)\n * @method static mixed get(array|string $key, $default = null)\n * @method static array all()\n * @method static void set(array|string $key, $value)\n * @method static void prepend(string $key, $value)\n * @method static void push(string $key, $value)\n * @method static array parseConfigKey(string $key)\n * @method static void package(string $namespace, string $hint)\n * @method static void afterLoading(string $namespace, \\Closure $callback)\n * @method static void addNamespace(string $namespace, string $hint)\n * @method static array getNamespaces()\n * @method static \\October\\Rain\\Config\\LoaderInterface getLoader()\n * @method static void setLoader(\\October\\Rain\\Config\\LoaderInterface $loader)\n * @method static string getEnvironment()\n * @method static array getAfterLoadCallbacks()\n * @method static array getItems()\n * @method static bool offsetExists(string $key)\n * @method static mixed offsetGet(string $key)\n * @method static void offsetSet(string $key, mixed $value)\n * @method static void offsetUnset(string $key)\n *\n * @see \\October\\Rain\\Config\\Repository\n */\nclass Config extends Facade\n{\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'config';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Currency.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse October\\Rain\\Support\\Facade;\n\n/**\n * Currency facade\n *\n * @method static mixed getDefault()\n * @method static string getDefaultCode()\n * @method static mixed getPrimary()\n * @method static string getPrimaryCode()\n * @method static mixed getActive()\n * @method static string getActiveCode()\n * @method static mixed listConverters(bool $asObject)\n * @method static mixed listConverterObjects()\n * @method static mixed findConverterByAlias()\n * @method static bool isModelMultisite($model, $attribute = null)\n *\n * @see \\Responsiv\\Currency\\Classes\\CurrencyManager\n */\nclass Currency extends Facade\n{\n    /**\n     * getFacadeAccessor gets the registered name of the component.\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'currencies';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/DbDongle.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse October\\Rain\\Support\\Facade;\n\n/**\n * DbDongle\n *\n * @method static mixed raw(string $sql, array $params = null)\n * @method static string rawValue(string $sql)\n * @method static string parse(string $sql, array $params = null)\n * @method static string parseGroupConcat(string $sql)\n * @method static string parseConcat(string $sql)\n * @method static string parseIfNull(string $sql)\n * @method static string parseBooleanExpression(string $sql)\n * @method static string cast(string $sql, string $asType = 'INTEGER')\n * @method static void convertTimestamps(string $table, string|array $columns = null)\n * @method static void disableStrictMode()\n * @method static string getDriver()\n * @method static string getTablePrefix()\n *\n * @see \\October\\Rain\\Database\\Dongle\n */\nclass DbDongle extends Facade\n{\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'db.dongle';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Event.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse Model;\nuse Cache;\nuse October\\Rain\\Events\\FakeDispatcher;\nuse Illuminate\\Support\\Facades\\Event as EventBase;\n\n/**\n * Event\n *\n * @see \\October\\Rain\\Events\\PriorityDispatcher\n */\nclass Event extends EventBase\n{\n    /**\n     * fake the instance\n     */\n    public static function fake($eventsToFake = [])\n    {\n        static::swap($fake = new FakeDispatcher(static::getFacadeRoot(), $eventsToFake));\n\n        Model::setEventDispatcher($fake);\n        Cache::refreshEventDispatcher();\n\n        return $fake;\n    }\n\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'events.priority';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/File.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse Illuminate\\Support\\Facades\\File as FileBase;\n\n/**\n * File\n *\n * @method static string anyname(string $anyname) anyname extracts the path and filename without extension\n * @method static bool isDirectoryEmpty(string $directory)\n * @method static string sizeToString(int $bytes)\n * @method static string localToPublic(string $path)\n * @method static string isLocalPath(string $path, bool $realpath)\n * @method static string fromClass(mixed $className)\n * @method static string|bool existsInsensitive(string $path)\n * @method static string symbolizePath(string $path, mixed $default)\n * @method static bool isPathSymbol(string $path)\n *\n * @see \\October\\Rain\\Filesystem\\Filesystem\n */\nclass File extends FileBase\n{\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'files';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Flash.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse October\\Rain\\Support\\Facade;\n\n/**\n * Flash\n *\n * @method static bool check()\n * @method static array all(string $format = null)\n * @method static array get(string $key, string $format = null)\n * @method static array|\\October\\Rain\\Flash\\FlashBag error(string $message = null)\n * @method static array|\\October\\Rain\\Flash\\FlashBag success(string $message = null)\n * @method static array|\\October\\Rain\\Flash\\FlashBag warning(string $message = null)\n * @method static array|\\October\\Rain\\Flash\\FlashBag info(string $message = null)\n * @method static \\October\\Rain\\Flash\\FlashBag add(string $key, string $message)\n * @method static void store()\n * @method static void forget(string $key = null)\n * @method static void purge()\n *\n * @see \\October\\Rain\\Flash\\FlashBag\n */\nclass Flash extends Facade\n{\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'flash';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Form.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse October\\Rain\\Support\\Facade;\n\n/**\n * Form\n *\n * @method static string open(array $options = [])\n * @method static string ajax(string $handler, array $options = [])\n * @method static string model(mixed $model, array $options = [])\n * @method static void setModel(mixed $model)\n * @method static string close()\n * @method static string token()\n * @method static string label(string $name, string $value = null, array $options = [])\n * @method static string input(string $type, string $name, string $value = null, array $options = [])\n * @method static string text(string $name, string $value, array $options = [])\n * @method static string password(string $name, array $options = [])\n * @method static string hidden(string $name, string $value = null, array $options = [])\n * @method static string email(string $name, string $value = null, array $options = [])\n * @method static string url(string $name, string $value = null, array $options = [])\n * @method static string file(string $name, array $options = [])\n * @method static string textarea(string $name, string $value = null, array $options = [])\n * @method static string select(string $name, array $list = [], string $value = null, array $options = [])\n * @method static string selectRange(string $name, string $begin, string $end, string $selected = null, array $options = [])\n * @method static string selectYear()\n * @method static string selectMonth(string $name, string $selected = null, array $options = [], string $format = '%B')\n * @method static string getSelectOption(string|array $display, string $value, string $selected)\n * @method static string checkbox(string $name, $value = 1, bool $checked = null, array $options = [])\n * @method static string radio(string $name, $value = null, bool $checked = null, array $options = [])\n * @method static string reset(string $value, array $attributes = [])\n * @method static string image(string $url, string $name = null, array $attributes = [])\n * @method static string button(string $value = null, array $options = [])\n * @method static string getIdAttribute(string $name, array $attributes)\n * @method static string getValueAttribute(string $name, string $value = null)\n * @method static string old(string $name)\n * @method static bool oldInputIsEmpty()\n * @method static \\Illuminate\\Session\\Store getSessionStore()\n * @method static \\October\\Rain\\Html\\FormBuilder setSessionStore(\\Illuminate\\Session\\Store $session)\n * @method static string value(string $name, string $value = null)\n * @method static string sessionKey(string $sessionKey = null)\n * @method static string getSessionKey()\n *\n * @see \\October\\Rain\\Html\\FormBuilder\n */\nclass Form extends Facade\n{\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'form';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Html.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse October\\Rain\\Support\\Facade;\n\n/**\n * Html\n *\n * @method static string entities(string $value)\n * @method static string decode(string $value)\n * @method static string script(string $url, array $attributes, bool $secure = null)\n * @method static string style(string $url, array $attributes = [], bool $secure = null)\n * @method static string image(string $url, string $alt = null, array $attributes = [], bool $secure = null)\n * @method static string link(string $url, string $title = null, array $attributes = [], bool $secure = null)\n * @method static string secureLink(string $url, string $title = null, array $attributes = [])\n * @method static string linkAsset(string $url, string $title = null, array $attributes = [], bool $secure = null)\n * @method static string linkSecureAsset(string $url, string $title = null, array $attributes = [])\n * @method static string linkRoute(string $name, string $title = null, array $parameters = [], array $attributes = [])\n * @method static string linkAction(string $action, string $title = null, array $parameters = [], array $attributes = [])\n * @method static string mailto(string $email, string $title = null, array $attributes = [])\n * @method static string email(string $email)\n * @method static string ol(array $list, array $attributes = [])\n * @method static string ul(array $list, array $attributes = [])\n * @method static string attributes(array $attributes)\n * @method static string obfuscate(string $value)\n * @method static string strip(string $string)\n * @method static string limit(string $html, int $maxLength = 100, string $end = '...')\n * @method static string clean(string $html)\n *\n * @see \\October\\Rain\\Html\\HtmlBuilder\n */\nclass Html extends Facade\n{\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'html';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Ini.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse October\\Rain\\Support\\Facade;\n\n/**\n * Ini\n *\n * @method static array parse(string $contents)\n * @method static array parseFile(string $fileName)\n * @method static string render(array $vars = [], int $level = 1)\n *\n * @see \\October\\Rain\\Parse\\Ini\n */\nclass Ini extends Facade\n{\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'parse.ini';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Input.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse October\\Rain\\Support\\Facade;\n\n/**\n * Input\n *\n * @see \\Illuminate\\Http\\Request\n */\nclass Input extends Facade\n{\n    /**\n     * get an item from the input data\n     * This method is used for all request verbs (GET, POST, PUT, and DELETE)\n     *\n     * @param  string|null  $key\n     * @param  mixed   $default\n     * @return mixed\n     */\n    public static function get($key = null, $default = null)\n    {\n        return static::$app['request']->input($key, $default);\n    }\n\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'request';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Mail.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse October\\Rain\\Mail\\FakeMailer;\nuse Illuminate\\Support\\Facades\\Mail as MailBase;\n\n/**\n * Mail\n *\n * @method static void sendTo(mixed $recipients, string $view, array $data = [], $callback = null, $options = [])\n *\n * @see \\October\\Rain\\Mails\\Dispatcher\n */\nclass Mail extends MailBase\n{\n    /**\n     * fake the instance\n     */\n    public static function fake()\n    {\n        static::swap($fake = new FakeMailer);\n\n        return $fake;\n    }\n\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'mail.manager';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Markdown.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse October\\Rain\\Support\\Facade;\n\n/**\n * Markdown\n *\n * @method static string parse(string $text)\n * @method static string parseClean(string $text)\n * @method static string parseSafe(string $text)\n * @method static string parseLine(string $text)\n *\n * @see \\October\\Rain\\Parse\\Markdown\n */\nclass Markdown extends Facade\n{\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'parse.markdown';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Resizer.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse October\\Rain\\Support\\Facade;\n\n/**\n * Resizer\n *\n * @see \\October\\Rain\\Resize\\Resizer\n */\nclass Resizer extends Facade\n{\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'resizer';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Schema.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse Illuminate\\Support\\Facades\\Schema as SchemaBase;\n\n/**\n * Schema\n *\n * @see \\Illuminate\\Database\\Schema\\Builder\n */\nclass Schema extends SchemaBase\n{\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'db.schema';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Site.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse October\\Rain\\Support\\Facade;\n\n/**\n * Site facade\n *\n * @method static bool hasFeature(string $name)\n * @method static mixed getSiteFromRequest(string $host, string $uri)\n * @method static mixed getSiteFromId($id)\n * @method static mixed getPrimarySite()\n * @method static mixed getAnySite()\n * @method static bool hasAnySite()\n * @method static bool hasMultiSite()\n * @method static bool hasSiteGroups()\n * @method static \\System\\Classes\\SiteCollection listEnabled()\n * @method static \\System\\Classes\\SiteCollection listSites()\n * @method static array listSiteIds()\n * @method static array listSiteIdsInGroup($siteId)\n * @method static array listSiteIdsInLocale($siteId)\n * @method static mixed getEditSite()\n * @method static string getEditSiteId()\n * @method static void setEditSiteId(string $siteId)\n * @method static void setEditSite(mixed $site)\n * @method static mixed getAnyEditSite()\n * @method static bool hasAnyEditSite()\n * @method static bool hasMultiEditSite()\n * @method static \\System\\Classes\\SiteCollection listEditEnabled()\n * @method static void applyEditSite(mixed $site)\n * @method static mixed getActiveSite()\n * @method static string getActiveSiteId()\n * @method static void setActiveSiteId(string $siteId)\n * @method static void setActiveSite(mixed $site)\n * @method static void applyActiveSite(mixed $site)\n * @method static int|null getSiteIdFromContext()\n * @method static int getSiteGroupIdFromContext()\n * @method static string|null getSiteCodeFromContext()\n * @method static mixed getSiteFromContext()\n * @method static bool hasGlobalContext()\n * @method static mixed withGlobalContext(callable $callback)\n * @method static mixed withContext($siteId, callable $callback)\n * @method static mixed getSiteFromBrowser(string $acceptLanguage)\n * @method static mixed getSiteForLocale(string $locale)\n * @method static void resetCache()\n *\n * @see \\System\\Classes\\SiteManager\n */\nclass Site extends Facade\n{\n    /**\n     * getFacadeAccessor gets the registered name of the component.\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'system.sites';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Str.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse October\\Rain\\Support\\Facade;\n\n/**\n * Str\n *\n * @method static string after(string $subject, string $search)\n * @method static string ascii(string $value, string $language = 'en')\n * @method static string before(string $subject, string $search)\n * @method static string camel(string $value)\n * @method static bool contains(string $haystack, string|array $needles)\n * @method static bool endsWith(string $haystack, string|array $needles)\n * @method static string finish(string $value, string $cap)\n * @method static bool is(string|array $pattern, string $value)\n * @method static string kebab(string $value)\n * @method static int length(string $value, string $encoding = null)\n * @method static string limit(string $value, int $limit = 100, string $end = '...')\n * @method static string lower(string $value)\n * @method static string words(string $value, int $words = 100, $end = '...')\n * @method static array parseCallback(string $callback, string $default = null)\n * @method static string plural(string $value, int $count)\n * @method static string random(int $length = 16)\n * @method static string replaceArray(string $search, array $replace, string $subject)\n * @method static string replaceFirst(string $search, string $replace, string $subject)\n * @method static string replaceLast(string $search, string $replace, string $subject)\n * @method static string start(string $value, string $prefix)\n * @method static string upper(string $value)\n * @method static string title(string $value)\n * @method static string singular(string $value)\n * @method static string slug(string $title, string $seperator = '-', string $language = 'en')\n * @method static string snake(string $value, string $delimiter = '_')\n * @method static bool startsWith(string $haystack, string|array $needles)\n * @method static string studly(string $value)\n * @method static string substr(string $string, int $start, int $length = null)\n * @method static string ucfirst(string $string)\n * @method static string ordinal(int $number)\n * @method static string normalizeEol(string $string)\n * @method static string normalizeClassName(string $name)\n * @method static string getClassId(string $name)\n * @method static string getClassNamespace(string $name)\n * @method static int getPrecedingSymbols(string $string, string $symbol)\n *\n * @see \\October\\Rain\\Support\\Str\n */\nclass Str extends Facade\n{\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'string';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Twig.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse October\\Rain\\Support\\Facade;\n\n/**\n * Twig\n *\n * @method static string parse(string $contents, array $vars = [])\n *\n * @see \\October\\Rain\\Parse\\Twig\n */\nclass Twig extends Facade\n{\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'parse.twig';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Url.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse Illuminate\\Support\\Facades\\URL as UrlBase;\n\n/**\n * Url\n *\n * @method static string assetVersion(string $path)\n *\n * @see \\Illuminate\\Routing\\UrlGenerator\n * @see \\October\\Rain\\Html\\UrlMixin\n */\nclass Url extends UrlBase\n{\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'url';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Validator.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse Illuminate\\Support\\Facades\\Validator as ValidatorBase;\n\n/**\n * Validator\n *\n * @see \\October\\Rain\\Validation\\Factory\n */\nclass Validator extends ValidatorBase\n{\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'validator';\n    }\n}\n"
  },
  {
    "path": "src/Support/Facades/Yaml.php",
    "content": "<?php namespace October\\Rain\\Support\\Facades;\n\nuse October\\Rain\\Support\\Facade;\n\n/**\n * Yaml\n *\n * @method static array parse(string $contents)\n * @method static array parseFile(string $fileName)\n * @method static array parseFileCached(string $fileName)\n * @method static string render(array $vars, array $options = [])\n *\n * @see \\October\\Rain\\Parse\\Yaml\n */\nclass Yaml extends Facade\n{\n    /**\n     * getFacadeAccessor returns the registered name of the component\n     * @return string\n     */\n    protected static function getFacadeAccessor()\n    {\n        return 'parse.yaml';\n    }\n}\n"
  },
  {
    "path": "src/Support/ModuleServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Support;\n\nuse Illuminate\\Console\\Application as Artisan;\nuse October\\Contracts\\Support\\OctoberPackage;\nuse October\\Rain\\Support\\ServiceProvider as ServiceProviderBase;\n\n/**\n * ModuleServiceProvider\n *\n * @package october\\support\n * @author Alexey Bobkov, Samuel Georges\n */\nabstract class ModuleServiceProvider extends ServiceProviderBase implements OctoberPackage\n{\n    /**\n     * register the service provider\n     */\n    public function register()\n    {\n        $module = $this->getModule(func_get_args());\n        if (!$module) {\n            return;\n        }\n\n        $modulePath = base_path('modules/' . $module);\n\n        // Register configuration path\n        $configPath = $modulePath . '/config';\n        if (!$this->app->configurationIsCached() && is_dir($configPath)) {\n            $this->loadConfigFrom($configPath, $module);\n        }\n\n        // Register view path\n        $viewsPath = $modulePath . '/views';\n        if (is_dir($viewsPath)) {\n            $this->loadViewsFrom($viewsPath, $module);\n        }\n\n        // Load translator\n        $this->loadTranslationsFrom($modulePath . '/lang', $module);\n        if ($this->app->runningInBackend()) {\n            $this->loadJsonTranslationsFrom($modulePath . '/lang');\n        }\n\n        // Add routes, if available\n        $routesFile = $modulePath . '/routes.php';\n        if (!$this->app->routesAreCached() && file_exists($routesFile)) {\n            $this->loadRoutesFrom($routesFile);\n        }\n    }\n\n    /**\n     * boot bootstraps the application events\n     */\n    public function boot()\n    {\n        $module = $this->getModule(func_get_args());\n        if (!$module) {\n            return;\n        }\n\n        // Reserved for boot logic\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function registerMarkupTags()\n    {\n        return [];\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function registerComponents()\n    {\n        return [];\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function registerPageSnippets()\n    {\n        return [];\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function registerContentFields()\n    {\n        return [];\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function registerNavigation()\n    {\n        return [];\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function registerPermissions()\n    {\n        return [];\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function registerSettings()\n    {\n        return [];\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function registerSchedule($schedule)\n    {\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function registerReportWidgets()\n    {\n        return [];\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function registerFormWidgets()\n    {\n        return [];\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function registerFilterWidgets()\n    {\n        return [];\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function registerListColumnTypes()\n    {\n        return [];\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function registerMailLayouts()\n    {\n        return [];\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function registerMailTemplates()\n    {\n        return [];\n    }\n\n    /**\n     * @inheritDoc\n     */\n    public function registerMailPartials()\n    {\n        return [];\n    }\n\n    /**\n     * getModule gets the module name from method args\n     */\n    protected function getModule($args)\n    {\n        return isset($args[0]) && is_string($args[0]) ? $args[0] : null;\n    }\n\n    /**\n     * discoverConsoleCommands automatically finds and registers console\n     * commands from the module's console directory\n     */\n    protected function discoverConsoleCommands(string $module): void\n    {\n        Artisan::starting(function ($artisan) use ($module) {\n            $consolePath = base_path('modules/' . $module . '/console');\n\n            if (!is_dir($consolePath)) {\n                return;\n            }\n\n            $namespace = ucfirst($module) . '\\\\Console\\\\';\n            $commands = [];\n\n            foreach (glob($consolePath . '/*.php') as $file) {\n                $className = $namespace . basename($file, '.php');\n\n                if (!class_exists($className)) {\n                    continue;\n                }\n\n                $reflection = new \\ReflectionClass($className);\n\n                if (\n                    $reflection->isSubclassOf(\\Illuminate\\Console\\Command::class) &&\n                    !$reflection->isAbstract()\n                ) {\n                    $commands[] = $className;\n                }\n            }\n\n            $artisan->resolveCommands($commands);\n        });\n    }\n\n    /**\n     * registerConsoleCommand registers a new console (artisan) command\n     */\n    protected function registerConsoleCommand(string $key, string $class)\n    {\n        $key = 'command.'.$key;\n\n        $this->app->singleton($key, function ($app) use ($class) {\n            return $this->app->make($class);\n        });\n\n        $this->commands($key);\n    }\n\n    /**\n     * loadConfigFrom registers a config file namespace\n     * @param  string  $path\n     * @param  string  $namespace\n     */\n    protected function loadConfigFrom($path, $namespace)\n    {\n        $this->app['config']->package($namespace, $path);\n    }\n}\n"
  },
  {
    "path": "src/Support/README.md",
    "content": "## Rain Support\n\nThe October Rain Support contains common classes relevant to supporting the other October Rain libraries. It adds the following features:\n\n### Scaffolding\n\nSee the Scaffolding Commands section of the [Console documentation](https://octobercms.com/docs/console/commands).\n\n### A true Singleton trait\n\nA *true singleton* is a class that can ever only have a single instance, no matter what. Use it in your classes like this:\n\n    class MyClass\n    {\n        use \\October\\Rain\\Support\\Traits\\Singleton;\n    }\n\n    $class = MyClass::instance();\n\n### Global helpers\n\n**input()**\n\nSimilar to `Input::get()` this returns an input parameter or the default value. However it supports HTML Array names. Booleans are also converted from strings.\n\n    $value = input('value', 'not found');\n    $name = input('contact[name]');\n    $city = input('contact[location][city]');\n\n### Event emitter\n\nAdds event related features to any class.\n\n**Attach to a class**\n\n    class MyClass\n    {\n        use October\\Rain\\Support\\Traits\\Emitter;\n    }\n\n**Bind to an event**\n\n    $myObject = new MyClass;\n    $myObject->bindEvent('cook.bacon', function(){\n        echo 'Bacon is ready';\n    });\n\n**Trigger an event**\n\n    // Outputs: Bacon is ready\n    $myObject->fireEvent('cook.bacon');\n\n**Bind to an event only once**\n\n    $myObject = new MyClass;\n    $myObject->bindEvent('cook.soup', function(){\n        echo 'Soup is ready. Want more? NO SOUP FOR YOU!';\n    }, true);\n\n**Bind an event to other object method**\n\n    $myObject->bindEvent('cook.eggs', [$anotherObject, 'methodToCookEggs']);\n\n**Unbind an event**\n\n    $myObject->unbindEvent('cook.bacon');\n    $myObject->unbindEvent(['cook.bacon', 'cook.eggs']);\n"
  },
  {
    "path": "src/Support/SafeCollection.php",
    "content": "<?php namespace October\\Rain\\Support;\n\n/**\n * SafeCollection is retained as a backwards-compatible alias for\n * System\\Twig\\SecurityPolicy\\SafeCollection. New code should reference\n * the System namespace directly.\n *\n * @deprecated Use \\System\\Twig\\SecurityPolicy\\SafeCollection instead.\n * @package october\\support\n */\nclass SafeCollection extends \\System\\Twig\\SecurityPolicy\\SafeCollection\n{\n}\n"
  },
  {
    "path": "src/Support/ServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Support;\n\nuse Illuminate\\Support\\ServiceProvider as ServiceProviderBase;\n\n/**\n * ServiceProvider is an empty umbrella class\n *\n * @package october\\support\n * @author Alexey Bobkov, Samuel Georges\n */\nabstract class ServiceProvider extends ServiceProviderBase\n{\n    /**\n     * @var \\October\\Rain\\Foundation\\Application app instance\n     */\n    protected $app;\n\n    /**\n     * callBeforeResolving sets up a before resolving listener, or fire immediately\n     * if already resolved.\n     *\n     * @param  string  $name\n     * @param  callable  $callback\n     * @return void\n     */\n    protected function callBeforeResolving($name, $callback)\n    {\n        $this->app->beforeResolving($name, $callback);\n\n        if ($this->app->resolved($name)) {\n            $callback($this->app->make($name), $this->app);\n        }\n    }\n\n    /**\n     * Get the default providers for a Laravel application.\n     *\n     * @return \\October\\Rain\\Support\\DefaultProviders\n     */\n    public static function defaultProviders()\n    {\n        return new DefaultProviders;\n    }\n}\n"
  },
  {
    "path": "src/Support/Singleton.php",
    "content": "<?php namespace October\\Rain\\Support;\n\nuse App;\n\n/**\n * @deprecated use App::singleton() with a manually built class\n */\nclass Singleton\n{\n    /**\n     * __construct\n     */\n    final protected function __construct()\n    {\n        $this->init();\n    }\n\n    /**\n     * instance creates a new instance of this singleton\n     */\n    final public static function instance()\n    {\n        $accessor = static::getSingletonAccessor();\n\n        if (!App::bound($accessor)) {\n            App::singleton($accessor, function () {\n                return static::getSingletonInstance();\n            });\n        }\n\n        return App::make($accessor);\n    }\n\n    /**\n     * getSingletonAccessor should return a meaningful IoC container code.\n     * Eg: backend.helper\n     */\n    protected static function getSingletonAccessor()\n    {\n        return get_called_class();\n    }\n\n    /**\n     * getSingletonInstance returns the final instance of this singleton\n     */\n    final public static function getSingletonInstance()\n    {\n        return new static;\n    }\n\n    /**\n     * init the singleton free from constructor parameters\n     */\n    protected function init()\n    {\n    }\n\n    /**\n     * __clone\n     * @ignore\n     */\n    public function __clone()\n    {\n        trigger_error('Cloning '.__CLASS__.' is not allowed.', E_USER_ERROR);\n    }\n\n    /**\n     * __wakeup\n     * @ignore\n     */\n    public function __wakeup()\n    {\n        trigger_error('Unserializing '.__CLASS__.' is not allowed.', E_USER_ERROR);\n    }\n}\n"
  },
  {
    "path": "src/Support/Str.php",
    "content": "<?php namespace October\\Rain\\Support;\n\nuse Illuminate\\Support\\Str as StrHelper;\nuse voku\\helper\\ASCII;\n\n/**\n * Str helper\n *\n * @package october\\support\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Str extends StrHelper\n{\n    /**\n     * slug adds extra sugar to convert slashes to separators\n     *\n     * @param  string  $title\n     * @param  string  $separator\n     * @param  string|null  $language\n     * @param  array<string, string>  $dictionary\n     * @return string\n     */\n    public static function slug($title, $separator = '-', $language = 'en', $dictionary = ['@' => 'at'])\n    {\n        $title = str_replace(['\\\\', '/'], ' ', (string) $title);\n\n        return parent::slug($title, $separator, $language, $dictionary);\n    }\n    /**\n     * ascii applies transliterate when the language is not found\n     *\n     * @param  string  $value\n     * @param  string  $language\n     * @return string\n     */\n    public static function ascii($value, $language = 'en')\n    {\n        return ASCII::to_ascii((string) $value, $language, true, false, true);\n    }\n\n    /**\n     * ordinal converts number to its ordinal English form\n     *\n     * This method converts 13 to 13th, 2 to 2nd ...\n     *\n     * @param integer $number Number to get its ordinal value\n     * @return string Ordinal representation of given string.\n     */\n    public static function ordinal($number)\n    {\n        if (in_array($number % 100, range(11, 13))) {\n            return $number.'th';\n        }\n\n        switch ($number % 10) {\n            case 1:\n                return $number.'st';\n            case 2:\n                return $number.'nd';\n            case 3:\n                return $number.'rd';\n            default:\n                return $number.'th';\n        }\n    }\n\n    /**\n     * shortNumber converts a large number to its abbreviated form. This\n     * method converts 1000 to 1K, 1500000 to 1.5M, etc.\n     */\n    public static function shortNumber($number, $precision = 1): string\n    {\n        if ($number < 1000) {\n            return (string) $number;\n        }\n\n        $units = ['', 'K', 'M', 'B', 'T'];\n        $power = (int) floor(log($number, 1000));\n        $value = $number / pow(1000, $power);\n\n        // Remove trailing .0 if any\n        $formattedValue = round($value, $precision);\n        if ($precision > 0) {\n            $formattedValue = rtrim(rtrim(number_format($formattedValue, $precision, '.', ''), '0'), '.');\n        }\n\n        return $formattedValue . $units[$power];\n    }\n\n    /**\n     * normalizeEol converts line breaks to a standard \\r\\n pattern\n     */\n    public static function normalizeEol($string)\n    {\n        return preg_replace('~\\R~u', \"\\r\\n\", $string);\n    }\n\n    /**\n     * normalizeClassName removes the starting slash from a class namespace \\\n     */\n    public static function normalizeClassName($name)\n    {\n        if (is_object($name)) {\n            $name = get_class($name);\n        }\n\n        return ltrim($name, '\\\\');\n    }\n\n    /**\n     * getClassId generates a class ID from either an object or a string of the class name\n     */\n    public static function getClassId($name)\n    {\n        if (is_object($name)) {\n            $name = get_class($name);\n        }\n\n        $name = ltrim($name, '\\\\');\n        $name = str_replace('\\\\', '_', $name);\n\n        return strtolower($name);\n    }\n\n    /**\n     * getClassNamespace returns a class namespace\n     */\n    public static function getClassNamespace($name)\n    {\n        $name = static::normalizeClassName($name);\n        return substr($name, 0, strrpos($name, \"\\\\\"));\n    }\n\n    /**\n     * getPrecedingSymbols checks if $string begins with any number of consecutive symbols,\n     * returns the number, otherwise returns 0\n     */\n    public static function getPrecedingSymbols(string $string, string $symbol): int\n    {\n        return strlen($string) - strlen(ltrim($string, $symbol));\n    }\n\n    /**\n     * limitMiddle limits the length of a string by removing characters from the middle\n     *\n     * @param  string  $value\n     * @param  int  $limit\n     * @param  string  $marker\n     * @return string\n     */\n    public static function limitMiddle($value, $limit = 100, $marker = '...')\n    {\n        if (mb_strwidth($value, 'UTF-8') <= $limit) {\n            return $value;\n        }\n\n        if ($limit > 3) {\n            $limit -= 3;\n        }\n\n        $limitStart = floor($limit / 2);\n        $limitEnd = $limit - $limitStart;\n\n        $valueStart = rtrim(mb_strimwidth($value, 0, $limitStart, '', 'UTF-8'));\n        $valueEnd = ltrim(mb_strimwidth($value, $limitEnd * -1, $limitEnd, '', 'UTF-8'));\n\n        return $valueStart . $marker . $valueEnd;\n    }\n}\n"
  },
  {
    "path": "src/Support/Traits/Emitter.php",
    "content": "<?php namespace October\\Rain\\Support\\Traits;\n\n/**\n * Emitter adds event related features to any class\n *\n * @package october\\support\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait Emitter\n{\n    /**\n     * @var array emitterSingleEventCollection of events to be fired once only\n     */\n    protected $emitterSingleEventCollection = [];\n\n    /**\n     * @var array emitterEventCollection of all registered events\n     */\n    protected $emitterEventCollection = [];\n\n    /**\n     * @var array emitterEventSorted collection\n     */\n    protected $emitterEventSorted = [];\n\n    /**\n     * bindEvent creates a new event binding\n     * @return void\n     */\n    public function bindEvent($event, $callback, $priority = 0)\n    {\n        $this->emitterEventCollection[$event][$priority][] = $callback;\n        unset($this->emitterEventSorted[$event]);\n    }\n\n    /**\n     * bindEventOnce creates a new event binding that fires once only\n     * @return void\n     */\n    public function bindEventOnce($event, $callback, $priority = 0)\n    {\n        $this->emitterSingleEventCollection[$event][$priority][] = $callback;\n        unset($this->emitterEventSorted[$event]);\n    }\n\n    /**\n     * unbindEvent destroys an event binding\n     * @return void\n     */\n    public function unbindEvent($event = null)\n    {\n        if (is_array($event)) {\n            foreach ($event as $_event) {\n                $this->unbindEvent($_event);\n            }\n            return;\n        }\n\n        if ($event === null) {\n            unset($this->emitterSingleEventCollection);\n            unset($this->emitterEventCollection);\n            unset($this->emitterEventSorted);\n            return;\n        }\n\n        unset($this->emitterSingleEventCollection[$event]);\n        unset($this->emitterEventCollection[$event]);\n        unset($this->emitterEventSorted[$event]);\n    }\n\n    /**\n     * fireEvent and call the listeners\n     * @param string $event Event name\n     * @param array $params Event parameters\n     * @param boolean $halt Halt after first non-null result\n     * @return array Collection of event results / Or single result (if halted)\n     */\n    public function fireEvent($event, $params = [], $halt = false)\n    {\n        if (!is_array($params)) {\n            $params = [$params];\n        }\n\n        // Micro optimization\n        if (\n            !isset($this->emitterEventCollection[$event]) &&\n            !isset($this->emitterSingleEventCollection[$event])\n        ) {\n            return $halt ? null : [];\n        }\n\n        if (!isset($this->emitterEventSorted[$event])) {\n            $this->emitterEventSorted[$event] = $this->emitterEventSortEvents($event);\n        }\n\n        $result = [];\n        foreach ($this->emitterEventSorted[$event] as $callback) {\n            $response = $callback(...$params);\n\n            if (!is_null($response) && $halt) {\n                return $response;\n            }\n\n            if ($response === false) {\n                break;\n            }\n\n            if (!is_null($response)) {\n                $result[] = $response;\n            }\n        }\n\n        if (isset($this->emitterSingleEventCollection[$event])) {\n            unset($this->emitterSingleEventCollection[$event]);\n            unset($this->emitterEventSorted[$event]);\n        }\n\n        return $halt ? null : $result;\n    }\n\n    /**\n     * emitterEventSortEvents sorts the listeners for a given event by priority\n     */\n    protected function emitterEventSortEvents(string $eventName, array $combined = []): array\n    {\n        if (isset($this->emitterEventCollection[$eventName])) {\n            foreach ($this->emitterEventCollection[$eventName] as $priority => $callbacks) {\n                $combined[$priority] = array_merge($combined[$priority] ?? [], $callbacks);\n            }\n        }\n\n        if (isset($this->emitterSingleEventCollection[$eventName])) {\n            foreach ($this->emitterSingleEventCollection[$eventName] as $priority => $callbacks) {\n                $combined[$priority] = array_merge($combined[$priority] ?? [], $callbacks);\n            }\n        }\n\n        krsort($combined);\n\n        return call_user_func_array('array_merge', $combined);\n    }\n}\n"
  },
  {
    "path": "src/Support/Traits/KeyParser.php",
    "content": "<?php namespace October\\Rain\\Support\\Traits;\n\n/**\n * KeyParser trait resolves key strings into namespace, group and item\n * Example: namespace::group.item\n *\n * @package october\\support\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait KeyParser\n{\n    /**\n     * @var array keyParserCache is a cache of the parsed items\n     */\n    protected $keyParserCache = [];\n\n    /**\n     * setParsedKey value\n     * @param  string  $key\n     * @param  array   $parsed\n     * @return void\n     */\n    public function setParsedKey($key, $parsed): void\n    {\n        $this->keyParserCache[$key] = $parsed;\n    }\n\n    /**\n     * parseKey into namespace, group, and item\n     */\n    public function parseKey(string $key): array\n    {\n        // If we've already parsed the given key, we'll return the cached version we\n        // already have, as this will save us some processing. We cache off every\n        // key we parse so we can quickly return it on all subsequent requests.\n        if (isset($this->keyParserCache[$key])) {\n            return $this->keyParserCache[$key];\n        }\n\n        $segments = explode('.', $key);\n\n        // If the key does not contain a double colon, it means the key is not in a\n        // namespace, and is just a regular configuration item. Namespaces are a\n        // tool for organizing configuration items for things such as modules.\n        if (strpos($key, '::') === false) {\n            $parsed = $this->keyParserParseBasicSegments($segments);\n        }\n        else {\n            $parsed = $this->keyParserParseSegments($key);\n        }\n\n        // Once we have the parsed array of this key's elements, such as its groups\n        // and namespace, we will cache each array inside a simple list that has\n        // the key and the parsed array for quick look-ups for later requests.\n        return $this->keyParserCache[$key] = $parsed;\n    }\n\n    /**\n     * keyParserParseBasicSegments as an array\n     */\n    protected function keyParserParseBasicSegments(array $segments): array\n    {\n        // The first segment in a basic array will always be the group, so we can go\n        // ahead and grab that segment. If there is only one total segment we are\n        // just pulling an entire group out of the array and not a single item.\n        $group = $segments[0];\n\n        if (count($segments) === 1) {\n            return [null, $group, null];\n        }\n\n        // If there is more than one segment in this group, it means we are pulling\n        // a specific item out of a groups and will need to return the item name\n        // as well as the group so we know which item to pull from the arrays.\n        $item = implode('.', array_slice($segments, 1));\n\n        return [null, $group, $item];\n    }\n\n    /**\n     * keyParserParseSegments from a string\n     */\n    protected function keyParserParseSegments(string $key): array\n    {\n        list($namespace, $item) = explode('::', $key);\n\n        // First we'll just explode the first segment to get the namespace and group\n        // since the item should be in the remaining segments. Once we have these\n        // two pieces of data we can proceed with parsing out the item's value.\n        $itemSegments = explode('.', $item);\n\n        $groupAndItem = array_slice($this->keyParserParseBasicSegments($itemSegments), 1);\n\n        return array_merge([$namespace], $groupAndItem);\n    }\n}\n"
  },
  {
    "path": "src/Support/Traits/Singleton.php",
    "content": "<?php namespace October\\Rain\\Support\\Traits;\n\n/**\n * Singleton trait allows a simple interface for treating a class as a singleton\n * Usage: myObject::instance()\n *\n * @package october\\support\n * @author Alexey Bobkov, Samuel Georges\n */\ntrait Singleton\n{\n    /**\n     * @var ?static instance\n     */\n    protected static $instance;\n\n    /**\n     * instance create a new instance of this singleton\n     */\n    final public static function instance()\n    {\n        return isset(static::$instance)\n            ? static::$instance\n            : static::$instance = new static;\n    }\n\n    /**\n     * forgetInstance if it exists\n     */\n    final public static function forgetInstance()\n    {\n        static::$instance = null;\n    }\n\n    /**\n     * __construct\n     */\n    final protected function __construct()\n    {\n        $this->init();\n    }\n\n    /**\n     * init the singleton free from constructor parameters\n     */\n    protected function init()\n    {\n    }\n\n    /**\n     * __clone\n     * @ignore\n     */\n    public function __clone()\n    {\n        trigger_error('Cloning '.__CLASS__.' is not allowed.', E_USER_ERROR);\n    }\n\n    /**\n     * __wakeup\n     * @ignore\n     */\n    public function __wakeup()\n    {\n        trigger_error('Unserializing '.__CLASS__.' is not allowed.', E_USER_ERROR);\n    }\n}\n"
  },
  {
    "path": "src/Translation/FileLoader.php",
    "content": "<?php namespace October\\Rain\\Translation;\n\nuse Illuminate\\Translation\\FileLoader as FileLoaderBase;\n\n/**\n * FileLoader specifies a custom location for overriding translations\n *\n * @package october\\translation\n * @author Alexey Bobkov, Samuel Georges\n */\nclass FileLoader extends FileLoaderBase\n{\n    /**\n     * @var string path is a single path for the loader.\n     *\n     * @todo Can be removed if Laravel >= 10\n     */\n    protected $path;\n\n    /**\n     * @var array paths are used by default for the loader.\n     *\n     * @todo Can be removed if Laravel >= 10\n     */\n    protected $paths;\n\n    /**\n     * loadNamespaceOverrides loads a local namespaced translation group for overrides\n     */\n    protected function loadNamespaceOverrides(array $lines, $locale, $group, $namespace)\n    {\n        $paths = (array) $this->path ?: $this->paths;\n\n        return collect($paths)\n            ->reduce(function ($output, $path) use ($lines, $locale, $group, $namespace) {\n                $namespace = str_replace('.', '/', $namespace);\n                $file = \"{$path}/{$namespace}/{$locale}/{$group}.php\";\n\n                if ($this->files->exists($file)) {\n                    return array_replace_recursive($lines, $this->files->getRequire($file));\n                }\n\n                return $lines;\n            }, []);\n    }\n}\n"
  },
  {
    "path": "src/Translation/README.md",
    "content": "# Translation\n\nAn extension of illuminate\\translation.\n\nModules and plugins can have localization files in the /lang directory. Plugin and module localization files are registered automatically.\n\n## Accessing localization strings\n\n```php\n// Get a localization string from the CMS module\necho Lang::get('cms::errors.page.not_found');\n\n// Get a localization string from the october/blog plugin.\necho Lang::get('october.blog::messages.post.added');\n```\n\n## Overriding localization strings\n\nSystem users can override localization strings without altering the modules' and plugins' files. This is done by adding localization files to the app/lang directory. To override a plugin's localization:\n\n```\napp\n  lang\n    en\n      vendorname\n        pluginname\n          file.php\n```\n\nExample: lang/en/october/blog/errors.php\n\nTo override a module's localization:\n\n```\napp\n  lang\n    en\n      modulename\n        file.php\n```\n\nExample: lang/en/cms/errors.php\n"
  },
  {
    "path": "src/Translation/TranslationServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Translation;\n\nuse Illuminate\\Support\\ServiceProvider;\nuse Illuminate\\Contracts\\Support\\DeferrableProvider;\n\n/**\n * TranslationServiceProvider is a custom translator implemenation based on Laravel\n  *\n * @package october\\translation\n * @author Alexey Bobkov, Samuel Georges\n */\nclass TranslationServiceProvider extends ServiceProvider implements DeferrableProvider\n{\n    /**\n     * register the service provider.\n     */\n    public function register()\n    {\n        $this->registerLoader();\n\n        $this->app->singleton('translator', function ($app) {\n            $loader = $app['translation.loader'];\n\n            // When registering the translator component, we'll need to set the default\n            // locale as well as the fallback locale. So, we'll grab the application\n            // configuration so we can easily get both of these values from there.\n            $locale = $app['config']['app.locale'];\n\n            $trans = new Translator($loader, $locale);\n\n            $trans->setFallback($app['config']['app.fallback_locale']);\n\n            return $trans;\n        });\n    }\n\n    /**\n     * registerLoader registers the line loader\n     */\n    protected function registerLoader()\n    {\n        $this->app->singleton('translation.loader', function ($app) {\n            return new FileLoader($app['files'], $app['path.lang']);\n        });\n    }\n\n    /**\n     * provides gets the services provided by the provider\n     */\n    public function provides()\n    {\n        return ['translator', 'translation.loader'];\n    }\n}\n"
  },
  {
    "path": "src/Translation/Translator.php",
    "content": "<?php namespace October\\Rain\\Translation;\n\nuse Illuminate\\Translation\\Translator as TranslatorBase;\n\n/**\n * Translator class\n *\n * @package october/translation\n * @author Alexey Bobkov, Samuel Georges\n */\nclass Translator extends TranslatorBase\n{\n    /**\n     * @var string CORE_LOCALE is the native system language\n     */\n    const CORE_LOCALE = 'en';\n\n    /**\n     * get the translation for the given key. This logic carbon copies the Laravel parent class\n     * with an additional check to proxy 'validation' messages to 'system::validation', and adds\n     * fallback support to JSON messages.\n     */\n    public function get($key, array $replace = [], $locale = null, $fallback = true)\n    {\n        if ($line = $this->getValidationSpecific($key, $replace, $locale)) {\n            return $line;\n        }\n\n        // This is debug code to determine if language keys are\n        // migrated to JSON or translated in the first place\n        //\n        // $locale = $locale ?: $this->locale;\n        // $val = parent::get($key, $replace, $locale, $fallback);\n        // if (!isset($this->loaded['*']['*'][$locale][$key])) {\n        //     return is_string($val) ? '→'.$val.'←' : $val;\n        // }\n        // return $val;\n\n        // Begin CC\n        $locale = $locale ?: $this->locale;\n\n        $this->load('*', '*', $locale);\n\n        $line = $this->loaded['*']['*'][$locale][$key] ?? null;\n\n        // Laravel notes that with JSON translations, there is no usage of a fallback language.\n        // The key is the translation. Here we extend the technology to add fallback support.\n        if ($fallback && $line === null && $this->fallback !== null) {\n            $this->load('*', '*', $this->fallback);\n            $line = $this->loaded['*']['*'][$this->fallback][$key] ?? null;\n        }\n\n        if (!isset($line)) {\n            [$namespace, $group, $item] = $this->parseKey($key);\n\n            $locales = $fallback ? $this->localeArray($locale) : [$locale];\n\n            foreach ($locales as $locale) {\n                if (!is_null($line = $this->getLine(\n                    $namespace, $group, $locale, $item, $replace\n                ))) {\n                    return $line;\n                }\n            }\n        }\n\n        return $this->makeReplacements($line ?: $key, $replace);\n    }\n\n    /**\n     * set a given language key value.\n     *\n     * @param array|string $key\n     * @param mixed $value\n     * @param string|null $locale\n     * @return void\n     */\n    public function set($key, $value = null, $locale = null)\n    {\n        if (is_array($key)) {\n            foreach ($key as $innerKey => $innerValue) {\n                $this->set($innerKey, $innerValue, $locale);\n            }\n        }\n        else {\n            $locale = $locale ?: $this->locale;\n\n            $this->loaded['*']['*'][$locale][$key] = $value;\n        }\n    }\n\n    /**\n     * getValidationSpecific checks the system namespace by default for \"validation\" keys\n     */\n    protected function getValidationSpecific($key, $replace, $locale)\n    {\n        if (\n            str_starts_with($key, 'validation.') &&\n            !str_starts_with($key, 'validation.custom.') &&\n            !str_starts_with($key, 'validation.attributes.')\n        ) {\n            $nativeKey = 'system::'.$key;\n            $line = $this->get($nativeKey, $replace, $locale);\n            if ($line !== $nativeKey) {\n                return $line;\n            }\n        }\n\n        return null;\n    }\n\n    /**\n     * trans returns the translation for a given key\n     *\n     * @param  array|string  $id\n     * @param  array   $parameters\n     * @param  string  $locale\n     * @return string\n     */\n    public function trans($id, array $parameters = [], $locale = null)\n    {\n        return $this->get($id, $parameters, $locale);\n    }\n\n    /**\n     * transChoice gets a translation according to an integer value\n     *\n     * @param  string  $id\n     * @param  int     $number\n     * @param  array   $parameters\n     * @param  string  $locale\n     * @return string\n     */\n    public function transChoice($id, $number, array $parameters = [], $locale = null)\n    {\n        return $this->choice($id, $number, $parameters, $locale);\n    }\n\n    /**\n     * localeArray gets the array of locales to be checked\n     *\n     * @param  string|null  $locale\n     * @return array\n     */\n    protected function localeArray($locale)\n    {\n        return array_filter([$locale ?: $this->locale, $this->fallback, static::CORE_LOCALE]);\n    }\n}\n"
  },
  {
    "path": "src/Validation/Concerns/FormatsMessages.php",
    "content": "<?php namespace October\\Rain\\Validation\\Concerns;\n\nuse Illuminate\\Support\\Str;\n\n/**\n * FormatsMessages is a modifier to the base trait, it implements\n * some extended technology to allow rule objects to specify methods\n * message() and replace() for custom messaging.\n *\n * @see \\Illuminate\\Validation\\Concerns\\FormatsMessages\n */\ntrait FormatsMessages\n{\n    /**\n     * getMessage message for a validation attribute and rule.\n     * @param  string  $attribute\n     * @param  string  $rule\n     * @return string\n     */\n    protected function getMessage($attribute, $rule)\n    {\n        $attributeWithPlaceholders = $attribute;\n\n        $attribute = $this->replacePlaceholderInString($attribute);\n\n        $inlineMessage = $this->getInlineMessage($attribute, $rule);\n\n        // First we will retrieve the custom message for the validation rule if one\n        // exists. If a custom validation message is being used we'll return the\n        // custom message, otherwise we'll keep searching for a valid message.\n        if (!is_null($inlineMessage)) {\n            return $inlineMessage;\n        }\n\n        $lowerRule = Str::snake($rule);\n\n        $customKey = \"validation.custom.{$attribute}.{$lowerRule}\";\n\n        $customMessage = $this->getCustomMessageFromTranslator(\n            in_array($rule, $this->sizeRules)\n                ? [$customKey.\".{$this->getAttributeType($attribute)}\", $customKey]\n                : $customKey\n        );\n\n        // First we check for a custom defined validation message for the attribute\n        // and rule. This allows the developer to specify specific messages for\n        // only some attributes and rules that need to get specially formed.\n        if ($customMessage !== $customKey) {\n            return $customMessage;\n        }\n\n        // Modification: Apply fallback message from extension class, if one exists.\n        if ($this->hasExtensionMethod($lowerRule, 'message')) {\n            return $this->callExtensionMethod($lowerRule, 'message');\n        }\n\n        // If the rule being validated is a \"size\" rule, we will need to gather the\n        // specific error message for the type of attribute being validated such\n        // as a number, file or string which all have different message types.\n        if (in_array($rule, $this->sizeRules)) {\n            return $this->getSizeMessage($attributeWithPlaceholders, $rule);\n        }\n\n        // Finally, if no developer specified messages have been set, and no other\n        // special messages apply for this rule, we will just pull the default\n        // messages out of the translator service for this validation rule.\n        $key = \"validation.{$lowerRule}\";\n\n        if ($key != ($value = $this->translator->get($key))) {\n            return $value;\n        }\n\n        return $this->getFromLocalArray(\n            $attribute,\n            $lowerRule,\n            $this->fallbackMessages\n        ) ?: $key;\n    }\n\n    /**\n     * makeReplacements replace all error message place-holders with actual values.\n     * @param  string  $message\n     * @param  string  $attribute\n     * @param  string  $rule\n     * @param  array  $parameters\n     * @return string\n     */\n    public function makeReplacements($message, $attribute, $rule, $parameters)\n    {\n        $message = $this->replaceAttributePlaceholder(\n            $message, $this->getDisplayableAttribute($attribute)\n        );\n\n        $lowerRule = Str::snake($rule);\n\n        $message = $this->replaceInputPlaceholder($message, $attribute);\n        $message = $this->replaceIndexPlaceholder($message, $attribute);\n        $message = $this->replacePositionPlaceholder($message, $attribute);\n\n        if (isset($this->replacers[$lowerRule])) {\n            return $this->callReplacer($message, $attribute, $lowerRule, $parameters, $this);\n        }\n        elseif (method_exists($this, $replacer = \"replace{$rule}\")) {\n            return $this->$replacer($message, $attribute, $rule, $parameters);\n        }\n\n        // Modification: Apply fallback replacer from extension class, if one exists.\n        if ($this->hasExtensionMethod($lowerRule, 'replace')) {\n            return $this->callExtensionMethod($lowerRule, 'replace', [$message, $attribute, $lowerRule, $parameters]);\n        }\n\n        return $message;\n    }\n\n    /**\n     * hasExtensionMethod determines if an extended rule has a given method.\n     */\n    protected function hasExtensionMethod(string $rule, string $methodName): bool\n    {\n        if (!isset($this->extensions[$rule]) || !is_string($this->extensions[$rule])) {\n            return false;\n        }\n\n        [$class, $method] = Str::parseCallback($this->extensions[$rule]);\n\n        if (!method_exists($class, $methodName)) {\n            return false;\n        }\n\n        return true;\n    }\n\n    /**\n     * callExtensionMethod calls a method for an extended rule and returns the result as a string.\n     */\n    protected function callExtensionMethod(string $rule, string $methodName, array $args = []): string\n    {\n        [$class, $method] = Str::parseCallback($this->extensions[$rule]);\n\n        return (string) call_user_func_array([$this->container->make($class), $methodName], $args);\n    }\n}\n"
  },
  {
    "path": "src/Validation/Factory.php",
    "content": "<?php namespace October\\Rain\\Validation;\n\nuse Illuminate\\Validation\\Factory as FactoryBase;\n\n/**\n * Factory resolves to the Rain flavored validator.\n */\nclass Factory extends FactoryBase\n{\n    /**\n     * resolve\n     */\n    protected function resolve(array $data, array $rules, array $messages, array $customAttributes)\n    {\n        if (is_null($this->resolver)) {\n            return new Validator($this->translator, $data, $rules, $messages, $customAttributes);\n        }\n\n        return call_user_func($this->resolver, $this->translator, $data, $rules, $messages, $customAttributes);\n    }\n}\n"
  },
  {
    "path": "src/Validation/ValidationServiceProvider.php",
    "content": "<?php namespace October\\Rain\\Validation;\n\nuse Illuminate\\Validation\\ValidationServiceProvider as ValidationServiceProviderBase;\n\n/**\n * ValidationServiceProvider extends the Laravel validation package.\n */\nclass ValidationServiceProvider extends ValidationServiceProviderBase\n{\n    /**\n     * registerValidationFactory is identical logic of the parent class\n     * but replaces the instance with our own factory.\n     */\n    protected function registerValidationFactory()\n    {\n        $this->app->singleton('validator', function ($app) {\n            $validator = new Factory($app['translator'], $app);\n\n            if (isset($app['db'], $app['validation.presence'])) {\n                $validator->setPresenceVerifier($app['validation.presence']);\n            }\n\n            // Replacers for custom rules in Validator class\n            $validator->replacer('unique_site', function ($message, $attribute, $rule, $parameters) {\n                return __('validation.unique', ['attribute' => $attribute]);\n            });\n\n            return $validator;\n        });\n    }\n}\n"
  },
  {
    "path": "src/Validation/Validator.php",
    "content": "<?php namespace October\\Rain\\Validation;\n\nuse Site;\nuse Illuminate\\Validation\\Rules\\Unique;\nuse Illuminate\\Validation\\Validator as ValidatorBase;\nuse October\\Rain\\Exception\\ValidationException;\n\n/**\n * Validator is a modifier to the base class, it extends validation rules\n * with extra methods for specifying messages and replacements inside the\n * class definition.\n */\nclass Validator extends ValidatorBase\n{\n    use \\October\\Rain\\Validation\\Concerns\\FormatsMessages;\n\n    /**\n     * @var string exception to throw upon failure.\n     */\n    protected $exception = ValidationException::class;\n\n    /**\n     * validateUniqueSite validates the uniqueness of an attribute value on a given database table,\n     * including a site ID condition check.\n     */\n    public function validateUniqueSite($attribute, $value, $parameters)\n    {\n        $this->requireParameterCount(1, $parameters, 'unique_site');\n\n        [$connection, $table, $idColumn] = $this->parseTable($parameters[0]);\n\n        // The second parameter position holds the name of the column that needs to\n        // be verified as unique. If this parameter isn't specified we will just\n        // assume that this column to be verified shares the attribute's name.\n        $column = $this->getQueryColumn($parameters, $attribute);\n\n        $id = null;\n\n        if (isset($parameters[2])) {\n            [$idColumn, $id] = $this->getUniqueIds($idColumn, $parameters);\n\n            if (!is_null($id)) {\n                $id = stripslashes($id);\n            }\n        }\n\n        // The presence verifier is responsible for counting rows within this store\n        // mechanism which might be a relational database or any other permanent\n        // data store like Redis, etc. We will use it to determine uniqueness.\n        $verifier = $this->getPresenceVerifier($connection);\n\n        $extra = $this->getUniqueExtra($parameters);\n\n        if ($this->currentRule instanceof Unique) {\n            $extra = array_merge($extra, $this->currentRule->queryCallbacks());\n        }\n\n        // Add the site extra\n        $extra['site_id'] = Site::getSiteIdFromContext();\n\n        return $verifier->getCount(\n            $table, $column, $value, $id, $idColumn, $extra\n        ) == 0;\n    }\n}\n"
  },
  {
    "path": "tests/Assetic/MockAsset.php",
    "content": "<?php\n\nuse October\\Rain\\Assetic\\Asset\\AssetInterface;\nuse October\\Rain\\Assetic\\Filter\\FilterInterface;\n\n/**\n * Class MockAsset\n *\n * This class implements the AssetInterface and can be used\n * to test Assetic filters.\n */\nclass MockAsset implements AssetInterface\n{\n    public $content;\n\n    public function __construct(string $content = '')\n    {\n        $this->content = $content;\n    }\n\n    public function ensureFilter(FilterInterface $filter): void\n    {\n    }\n\n    public function getFilters(): array\n    {\n        return [];\n    }\n\n    public function clearFilters(): void\n    {\n    }\n\n    public function load(?FilterInterface $additionalFilter = null): void\n    {\n    }\n\n    public function dump(?FilterInterface $additionalFilter = null): string\n    {\n        return $this->content ?? '';\n    }\n\n    public function getContent(): ?string\n    {\n        return $this->content;\n    }\n\n    public function setContent(?string $content): void\n    {\n        $this->content = $content;\n    }\n\n    public function getSourceRoot(): ?string\n    {\n        return null;\n    }\n\n    public function getSourcePath(): ?string\n    {\n        return null;\n    }\n\n    public function getSourceDirectory(): ?string\n    {\n        return null;\n    }\n\n    public function getTargetPath(): ?string\n    {\n        return null;\n    }\n\n    public function setTargetPath(?string $targetPath): void\n    {\n    }\n\n    public function getLastModified(): ?int\n    {\n        return null;\n    }\n\n    public function getVars(): array\n    {\n        return [];\n    }\n\n    public function setValues(array $values): void\n    {\n    }\n\n    public function getValues(): array\n    {\n        return [];\n    }\n}\n"
  },
  {
    "path": "tests/Assetic/StylesheetMinifyTest.php",
    "content": "<?php\n\nuse October\\Rain\\Assetic\\Filter\\StylesheetMinify;\n\ninclude __DIR__ . '/MockAsset.php';\n\nclass StylesheetMinifyTest extends TestCase\n{\n    public function testSpaceRemoval()\n    {\n        $input  = 'body{width: calc(99.9% * 1/1 - 0px); height: 0px;}';\n        $output = 'body{width:calc(99.9% * 1/1 - 0px);height:0px}';\n\n        $mockAsset = new MockAsset($input);\n        $result = new StylesheetMinify();\n        $result->filterDump($mockAsset);\n\n        $this->assertEquals($output, $mockAsset->getContent());\n    }\n\n    public function testEmptyClassPreserve()\n    {\n        $input = ''.\n        '.view { /*\n            * Text\n            */\n            /*\n            * Links\n            */\n            /*\n            * Table\n            */\n            /*\n            * Table cell\n            */\n            /*\n            * Images\n            */ }';\n\n        $output = '.view{}';\n\n        $mockAsset = new MockAsset($input);\n        $result = new StylesheetMinify();\n        $result->filterDump($mockAsset);\n\n        $this->assertEquals($output, $mockAsset->getContent());\n    }\n\n    public function testEmptyCommentPreserve()\n    {\n        $input = ''.\n        '\n        body { background: blue; }\n        /**/\n        .view { color: red; }';\n\n        $output = 'body{background:blue}/**/.view{color:red}';\n\n        $mockAsset = new MockAsset($input);\n        $result = new StylesheetMinify();\n        $result->filterDump($mockAsset);\n\n        $this->assertEquals($output, $mockAsset->getContent());\n    }\n\n    public function testSpecialCommentPreservation()\n    {\n        $input  = 'body {/*! Keep me */}';\n        $output = 'body{/*! Keep me */}';\n\n        $mockAsset = new MockAsset($input);\n        $result = new StylesheetMinify();\n        $result->filterDump($mockAsset);\n\n        $this->assertEquals($output, $mockAsset->getContent());\n    }\n\n    public function testCommentRemoval()\n    {\n        $input  = 'body{/* First comment */} /* Second comment */';\n        $output = 'body{}';\n\n        $mockAsset = new MockAsset($input);\n        $result = new StylesheetMinify();\n        $result->filterDump($mockAsset);\n\n        $this->assertEquals($output, $mockAsset->getContent());\n    }\n\n    public function testCommentPreservationInVar()\n    {\n        $input  = '--ring-inset: var(--empty, /*!*/ /*!*/);';\n        $output = '--ring-inset:var(--empty,/*!*/ /*!*/);';\n\n        $mockAsset = new MockAsset($input);\n        $result = new StylesheetMinify();\n        $result->filterDump($mockAsset);\n\n        $this->assertEquals($output, $mockAsset->getContent());\n    }\n\n    public function testMinifyPreservationInVar()\n    {\n        $input  = ''.\n            'select:focus {\n                --ring-inset: var(--empty, /*!*/ /*!*/);\n                --ring-offset-width: 0px;\n                --ring-offset-color: #fff;\n                border-color: red;\n            }';\n        $output = 'select:focus{--ring-inset:var(--empty,/*!*/ /*!*/);--ring-offset-width:0px;--ring-offset-color:#fff;border-color:red}';\n\n        $mockAsset = new MockAsset($input);\n        $result = new StylesheetMinify();\n        $result->filterDump($mockAsset);\n\n        $this->assertEquals($output, $mockAsset->getContent());\n    }\n\n    public function testUnitPreservationInVar()\n    {\n        $input  = '--offset-width: 0px';\n        $output = '--offset-width:0px';\n\n        $mockAsset = new MockAsset($input);\n        $result = new StylesheetMinify();\n        $result->filterDump($mockAsset);\n\n        $this->assertEquals($output, $mockAsset->getContent());\n    }\n\n    public function testAttributeSelectorsWithLess()\n    {\n        $input = ''.\n            '[class^=\"icon-\"]:before,\n            [class*=\" icon-\"]:before {\n                speak: none;\n            }\n            /* makes the font 33% larger relative to the icon container */\n            .icon-large:before {\n                speak: initial;\n            }';\n\n        $output = '[class^=\"icon-\"]:before,[class*=\" icon-\"]:before{speak:none}.icon-large:before{speak:initial}';\n\n        $mockAsset = new MockAsset($input);\n        $result = new StylesheetMinify();\n        $result->filterDump($mockAsset);\n\n        $this->assertEquals($output, $mockAsset->getContent());\n    }\n\n    public function testSourceMappingUrlWithSpecialComment()\n    {\n        $input = ''.\n            '/*! keep me */*,:after,:before { opacity: 1; }body {background: purple;}\n\n            /*# sourceMappingUrl*/';\n\n        $output = '/*! keep me */*,:after,:before{opacity:1}body{background:purple}';\n\n        $mockAsset = new MockAsset($input);\n        $result = new StylesheetMinify();\n        $result->filterDump($mockAsset);\n\n        $this->assertEquals($output, $mockAsset->getContent());\n    }\n}\n"
  },
  {
    "path": "tests/Benchmark/Database/DatabaseBench.php",
    "content": "<?php\n\nuse October\\Rain\\Database\\Dongle;\n\n/**\n * @BeforeMethods({\"init\"})\n * @Revs(1000)\n * @Iterations(5)\n */\nclass DatabaseBench\n{\n    /**\n     * @var Dongle dongle for MySQL\n     */\n    protected $dongleMysql;\n\n    /**\n     * @var Dongle dongle for SQLite\n     */\n    protected $dongleSqlite;\n\n    /**\n     * @var Dongle dongle for PostgreSQL\n     */\n    protected $donglePgsql;\n\n    /**\n     * init\n     */\n    public function init()\n    {\n        $this->dongleMysql = new Dongle('mysql');\n        $this->dongleSqlite = new Dongle('sqlite');\n        $this->donglePgsql = new Dongle('pgsql');\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchParseConcatMysql()\n    {\n        $this->dongleMysql->parseConcat(\"SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users\");\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchParseConcatSqlite()\n    {\n        $this->dongleSqlite->parseConcat(\"SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users\");\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchParseConcatPgsql()\n    {\n        $this->donglePgsql->parseConcat(\"SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users\");\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchParseGroupConcatMysql()\n    {\n        $this->dongleMysql->parseGroupConcat(\"SELECT GROUP_CONCAT(name SEPARATOR ', ') FROM tags\");\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchParseGroupConcatSqlite()\n    {\n        $this->dongleSqlite->parseGroupConcat(\"SELECT GROUP_CONCAT(name SEPARATOR ', ') FROM tags\");\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchParseGroupConcatPgsql()\n    {\n        $this->donglePgsql->parseGroupConcat(\"SELECT GROUP_CONCAT(name SEPARATOR ', ') FROM tags\");\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchParseIfNullMysql()\n    {\n        $this->dongleMysql->parseIfNull(\"SELECT IFNULL(nickname, username) FROM users\");\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchParseIfNullPgsql()\n    {\n        $this->donglePgsql->parseIfNull(\"SELECT IFNULL(nickname, username) FROM users\");\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchParseBooleanExpressionMysql()\n    {\n        $this->dongleMysql->parseBooleanExpression(\"SELECT * FROM users WHERE is_active = true AND is_deleted = false\");\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchParseBooleanExpressionSqlite()\n    {\n        $this->dongleSqlite->parseBooleanExpression(\"SELECT * FROM users WHERE is_active = true AND is_deleted = false\");\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchParseFullQuerySqlite()\n    {\n        $this->dongleSqlite->parse(\"SELECT CONCAT(first_name, ' ', last_name), IFNULL(nickname, 'N/A') FROM users WHERE is_active = true\");\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchParseFullQueryPgsql()\n    {\n        $this->donglePgsql->parse(\"SELECT CONCAT(first_name, ' ', last_name), IFNULL(nickname, 'N/A') FROM users WHERE is_active = true\");\n    }\n}\n"
  },
  {
    "path": "tests/Benchmark/GeneralBench.php",
    "content": "<?php\n\n/**\n * @BeforeMethods({\"init\"})\n * @Revs(1000)\n * @Iterations(5)\n */\nclass GeneralBench\n{\n    /**\n     * init\n     */\n    public function init()\n    {\n\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchA()\n    {\n        (new \\October\\Rain\\Parse\\Markdown)->parse('**Hello**');\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchB()\n    {\n        \\Str::markdown('**Hello**');\n    }\n}\n"
  },
  {
    "path": "tests/Benchmark/Parse/ParseBench.php",
    "content": "<?php\n\nuse October\\Rain\\Parse\\Ini;\nuse October\\Rain\\Parse\\Bracket;\nuse October\\Rain\\Parse\\Parsedown\\ParsedownExtra;\n\n/**\n * @BeforeMethods({\"init\"})\n * @Revs(1000)\n * @Iterations(5)\n */\nclass ParseBench\n{\n    /**\n     * @var Ini\n     */\n    protected $ini;\n\n    /**\n     * @var Bracket\n     */\n    protected $bracket;\n\n    /**\n     * @var ParsedownExtra\n     */\n    protected $parsedown;\n\n    /**\n     * @var string\n     */\n    protected $iniContent;\n\n    /**\n     * @var array\n     */\n    protected $iniArray;\n\n    /**\n     * @var string\n     */\n    protected $bracketTemplate;\n\n    /**\n     * @var array\n     */\n    protected $bracketData;\n\n    /**\n     * @var string\n     */\n    protected $markdownSimple;\n\n    /**\n     * @var string\n     */\n    protected $markdownComplex;\n\n    /**\n     * init\n     */\n    public function init()\n    {\n        $this->ini = new Ini;\n        $this->bracket = new Bracket;\n        $this->parsedown = new ParsedownExtra;\n\n        // INI content for parsing\n        $this->iniContent = <<<INI\ntitle = \"My Application\"\ndebug = true\nversion = 1.5\n\n[database]\nhost = localhost\nport = 3306\nname = october_db\n\n[cache]\ndriver = redis\nprefix = app_\nttl = 3600\nINI;\n\n        // Array for INI rendering\n        $this->iniArray = [\n            'title' => 'My Application',\n            'debug' => true,\n            'version' => 1.5,\n            'database' => [\n                'host' => 'localhost',\n                'port' => 3306,\n                'name' => 'october_db'\n            ],\n            'cache' => [\n                'driver' => 'redis',\n                'prefix' => 'app_',\n                'ttl' => 3600\n            ]\n        ];\n\n        // Bracket template\n        $this->bracketTemplate = <<<TPL\nHello {name}, welcome to {site}!\nYour account details:\n- Email: {email}\n- Role: {role}\n{items}\n  - {title}: {value}\n{/items}\nTPL;\n\n        // Bracket data\n        $this->bracketData = [\n            'name' => 'John Doe',\n            'site' => 'October CMS',\n            'email' => 'john@example.com',\n            'role' => 'Administrator',\n            'items' => [\n                ['title' => 'Posts', 'value' => '42'],\n                ['title' => 'Comments', 'value' => '128'],\n                ['title' => 'Likes', 'value' => '256']\n            ]\n        ];\n\n        // Simple markdown\n        $this->markdownSimple = '**Hello** _world_!';\n\n        // Complex markdown\n        $this->markdownComplex = <<<MD\n# Welcome to October CMS\n\nThis is a **bold** statement with _italic_ text.\n\n## Features\n\n- Easy to use\n- Powerful plugins\n- Beautiful themes\n\n### Code Example\n\n```php\necho \"Hello World\";\n```\n\n> A blockquote for emphasis\n\n[Visit our site](https://octobercms.com)\nMD;\n    }\n\n    //\n    // INI Benchmarks\n    //\n\n    /**\n     * @Subject\n     */\n    public function benchIniParse()\n    {\n        $this->ini->parse($this->iniContent);\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchIniRender()\n    {\n        $this->ini->render($this->iniArray);\n    }\n\n    //\n    // Bracket Benchmarks\n    //\n\n    /**\n     * @Subject\n     */\n    public function benchBracketParseSimple()\n    {\n        $this->bracket->parseString('Hello {name}!', ['name' => 'World']);\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchBracketParseWithLoop()\n    {\n        $this->bracket->parseString($this->bracketTemplate, $this->bracketData);\n    }\n\n    //\n    // Markdown Benchmarks (using ParsedownExtra directly to avoid facade dependency)\n    //\n\n    /**\n     * @Subject\n     */\n    public function benchMarkdownParseSimple()\n    {\n        $this->parsedown->text($this->markdownSimple);\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchMarkdownParseComplex()\n    {\n        $this->parsedown->text($this->markdownComplex);\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchMarkdownParseLine()\n    {\n        $this->parsedown->line($this->markdownSimple);\n    }\n}\n"
  },
  {
    "path": "tests/Benchmark/Router/RouterBench.php",
    "content": "<?php\n\nuse October\\Rain\\Router\\Router;\n\n/**\n * @BeforeMethods({\"init\"})\n * @Revs(1000)\n * @Iterations(5)\n */\nclass RouterBench\n{\n    /**\n     * @var array routes\n     */\n    protected $routes;\n\n    /**\n     * @var array routesCached\n     */\n    protected $routesCached;\n\n    /**\n     * @var array fixtures\n     */\n    protected $fixtures = [\n        '/blog',\n        '/blog/post',\n        '/blog/post/:post_id',\n        '/blog/post/:post_id?',\n        '/blog/post/:post_id?',\n        '/blog/post/:post_id?|^[a-z\\\\-]+$',\n        '/blog/post/:post_id|^[0-9]+$',\n        '/blog/post/:post_id|^[0-9]?$',\n        '/blog/post/:post_id/:post_slug|^my-slug-.*',\n        '/blog/post/:post_id?my-post',\n        '/blog/post/:post_id?my-post|^[a-z]+$',\n        '/color/:color/largecode/:largecode*/edit',\n        '/color/:color/largecode/:largecode*/create',\n        '/color/:color/largecode/:largecode*',\n        '/color/:color/largecode/:largecode*|^[a-z]+\\\\/[a-z]+$',\n        '/blog/:id*|^[0-9]+$',\n        '/blog/:page?*|^[0-9\\\\/]+$',\n        '/blog/:page?*|^[0-9]+$',\n        '/blog/:year/:month/:slug*',\n        '/blog/category/:category*/:page?*|^[0-9\\\\/]+$',\n        '/blog/:year|^\\\\d{4}$/:month|^\\\\d{2}$/:day|^\\\\d{2}$/:slug',\n        '/job/:type?request/:id',\n        '/profile/:username',\n        '/product/:category?/:id',\n        '/portfolio/:year?noYear/:category?noCategory/:budget?noBudget',\n        '/authors/:author_id|^[a-z\\\\-]+$/details',\n        '/authors/:author_id?/details',\n        '/authors/:author_id?/:details?',\n        '/authors/:author_id|^[a-z\\\\-]+$/details/:record_type?|^[0-9]+$',\n        '/authors/:author_id?my-author-id|^[a-z\\\\-]+$/:record_type?15|^[0-9]+$',\n    ];\n\n    /**\n     * init\n     */\n    public function init()\n    {\n        $router = new Router;\n\n        // Build padded routes\n        $routes = [];\n        foreach ($this->fixtures as $index => $rule) {\n            $routes['pad1'.$index] = '/pad1/'.$rule;\n            $routes['pad2'.$index] = '/pad2/'.$rule;\n            $routes['pad3'.$index] = '/pad3/'.$rule;\n            $routes['pad3'.$index] = '/pad4/'.$rule;\n            $routes['pad3'.$index] = '/pad5/'.$rule;\n        }\n\n        // Final target at end (120 routes)\n        foreach ($this->fixtures as $index => $rule) {\n            $routes['rule'.$index] = $rule;\n        }\n\n        // Register with router\n        foreach ($routes as $name => $rule) {\n            $router->route($name, $rule);\n        }\n\n        $this->routes = $routes;\n        $this->routesCached = $router->toArray();\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchRoute()\n    {\n        $router = new Router;\n\n        foreach ($this->routes as $index => $rule) {\n            $router->route('rule'.$index, $rule);\n        }\n\n        $router->match('authors/test/details');\n    }\n\n    /**\n     * @Subject\n     */\n    public function benchRouteCached()\n    {\n        $router = new Router;\n\n        $router->fromArray($this->routesCached);\n\n        $router->match('authors/test/details');\n    }\n}\n"
  },
  {
    "path": "tests/Database/DongleTest.php",
    "content": "<?php\n\nuse October\\Rain\\Database\\Dongle;\n\nclass DongleTest extends TestCase\n{\n    public function testSqliteParseConcat()\n    {\n        $dongle = new Dongle('sqlite');\n\n        $result = $dongle->parseConcat(\"concat(first_name, ' ', last_name)\");\n        $this->assertEquals(\"first_name || ' ' || last_name\", $result);\n\n        $result = $dongle->parseConcat(\"CONCAT(  first_name   , ' ',    last_name  )\");\n        $this->assertEquals(\"first_name || ' ' || last_name\", $result);\n\n        $result = $dongle->parseConcat('concat(\"#\", id, \" - \", amount, \"(\", currency_code, \")\")');\n        $this->assertEquals('\"#\" || id || \" - \" || amount || \"(\" || currency_code || \")\"', $result);\n\n        $result = $dongle->parseConcat(\"concat(year, ' ', make , ' ' , model)\");\n        $this->assertEquals(\"year || ' ' || make || ' ' || model\", $result);\n\n        $result = $dongle->parseConcat(\"concat(last_name, ', ', first_name)\");\n        $this->assertEquals(\"last_name || ', ' || first_name\", $result);\n\n        $result = $dongle->parseConcat(\"concat(',', last_name, '   ,   ', first_name, ',')\");\n        $this->assertEquals(\"',' || last_name || '   ,   ' || first_name || ','\", $result);\n\n        $result = $dongle->parseConcat(\"concat(last_name, ',\\' ', first_name)\");\n        $this->assertEquals(\"last_name || ',\\' ' || first_name\", $result);\n\n        $result = $dongle->parseConcat(\"group_concat(first_name, ' ', last_name)\");\n        $this->assertEquals(\"group_concat(first_name, ' ', last_name)\", $result);\n    }\n\n    public function testSqliteParseGroupConcat()\n    {\n        $dongle = new Dongle('sqlite');\n\n        $result = $dongle->parseGroupConcat(\"group_concat(first_name separator ', ')\");\n        $this->assertEquals(\"group_concat(first_name, ', ')\", $result);\n\n        $result = $dongle->parseGroupConcat(\"group_concat(sometable.first_name SEPARATOR ', ')\");\n        $this->assertEquals(\"group_concat(sometable.first_name, ', ')\", $result);\n\n        $result = $dongle->parseGroupConcat(\"group_concat(id separator ')')\");\n        $this->assertEquals(\"group_concat(id, ')')\", $result);\n\n        // @todo\n        // $result = $dongle->parseGroupConcat(\"group_concat(id order by name separator ',')\");\n        // $this->assertEquals(\"group_concat(id, ',') OVER (order by name)\", $result);\n    }\n\n    public function testPgsqlParseGroupConcat()\n    {\n        $dongle = new Dongle('pgsql');\n\n        $result = $dongle->parseGroupConcat(\"group_concat(first_name separator ', ')\");\n        $this->assertEquals(\"string_agg(first_name::VARCHAR, ', ')\", $result);\n\n        $result = $dongle->parseGroupConcat(\"group_concat(sometable.first_name SEPARATOR ', ')\");\n        $this->assertEquals(\"string_agg(sometable.first_name::VARCHAR, ', ')\", $result);\n\n        $result = $dongle->parseGroupConcat(\"group_concat(id separator ')')\");\n        $this->assertEquals(\"string_agg(id::VARCHAR, ')')\", $result);\n    }\n\n    public function testSqlSrvParseGroupConcat()\n    {\n        $dongle = new Dongle('sqlsrv');\n\n        $result = $dongle->parseGroupConcat(\"group_concat(first_name separator ', ')\");\n        $this->assertEquals(\"dbo.GROUP_CONCAT_D(first_name, ', ')\", $result);\n\n        $result = $dongle->parseGroupConcat(\"group_concat(sometable.first_name SEPARATOR ', ')\");\n        $this->assertEquals(\"dbo.GROUP_CONCAT_D(sometable.first_name, ', ')\", $result);\n\n        $result = $dongle->parseGroupConcat(\"group_concat(id separator ')')\");\n        $this->assertEquals(\"dbo.GROUP_CONCAT_D(id, ')')\", $result);\n    }\n\n    public function testSqliteParseBooleanExpression()\n    {\n        $dongle = new Dongle('sqlite');\n\n        $result = $dongle->parseBooleanExpression(\"select * from table where is_true = true\");\n        $this->assertEquals(\"select * from table where is_true = 1\", $result);\n\n        $result = $dongle->parseBooleanExpression(\"is_true = true and is_false <> true\");\n        $this->assertEquals(\"is_true = 1 and is_false <> 1\", $result);\n\n        $result = $dongle->parseBooleanExpression(\"is_true = true and is_false = false or is_whatever = 2\");\n        $this->assertEquals(\"is_true = 1 and is_false = 0 or is_whatever = 2\", $result);\n\n        $result = $dongle->parseBooleanExpression(\"select * from table where is_true = true\");\n        $this->assertEquals(\"select * from table where is_true = 1\", $result);\n    }\n\n    public function testSqlSrvParseIfNull()\n    {\n        $dongle = new Dongle('sqlsrv');\n\n        $result = $dongle->parseIfNull(\"select ifnull(1,0) from table\");\n        $this->assertEquals(\"select isnull(1,0) from table\", $result);\n\n        $result = $dongle->parseIfNull(\"select IFNULL(1,0) from table\");\n        $this->assertEquals(\"select isnull(1,0) from table\", $result);\n    }\n\n    public function testPgSrvParseIfNull()\n    {\n        $dongle = new Dongle('pgsql');\n\n        $result = $dongle->parseIfNull(\"select ifnull(1,0) from table\");\n        $this->assertEquals(\"select coalesce(1,0) from table\", $result);\n\n        $result = $dongle->parseIfNull(\"select IFNULL(1,0) from table\");\n        $this->assertEquals(\"select coalesce(1,0) from table\", $result);\n    }\n}\n"
  },
  {
    "path": "tests/Database/ModelAddersTest.php",
    "content": "<?php\n\nclass ModelAddersTest extends TestCase\n{\n    public function testAddCasts()\n    {\n        $model = new TestModel();\n\n        $this->assertEquals(['id' => 'int'], $model->getCasts());\n\n        $model->addCasts(['foo' => 'int']);\n\n        $this->assertEquals(['id' => 'int', 'foo' => 'int'], $model->getCasts());\n    }\n}\n"
  },
  {
    "path": "tests/Database/SortableTest.php",
    "content": "<?php\n\nclass SortableTest extends TestCase\n{\n    public function setUp(): void\n    {\n        $capsule = new Illuminate\\Database\\Capsule\\Manager;\n        $capsule->addConnection([\n            'driver'   => 'sqlite',\n            'database' => ':memory:',\n            'prefix'   => ''\n        ]);\n        $capsule->setAsGlobal();\n        $capsule->bootEloquent();\n    }\n\n    public function testOrderByIsAutomaticallyAdded()\n    {\n        $model = new TestModel;\n        $query = $model->newQuery()->toSql();\n\n        $this->assertEquals('select * from \"test\" order by \"test\".\"sort_order\" asc', $query);\n    }\n\n    public function testOrderByCanBeOverridden()\n    {\n        $model = new TestModel;\n        $query1 = $model->newQuery()->orderBy('name')->orderBy('email', 'desc')->toSql();\n        $query2 = $model->newQuery()->orderBy('sort_order')->orderBy('name')->toSql();\n\n        $this->assertEquals('select * from \"test\" order by \"name\" asc, \"email\" desc', $query1);\n        $this->assertEquals('select * from \"test\" order by \"sort_order\" asc, \"name\" asc', $query2);\n    }\n}\n\nclass TestModel extends \\October\\Rain\\Database\\Model\n{\n    use \\October\\Rain\\Database\\Traits\\Sortable;\n\n    protected $table = 'test';\n}\n"
  },
  {
    "path": "tests/Database/Traits/EncryptableTest.php",
    "content": "<?php\n\nuse Illuminate\\Encryption\\Encrypter;\n\nclass EncryptableTest extends TestCase\n{\n    const TEST_CRYPT_KEY = 'gBmM1S5bxZ5ePRj5';\n\n    public function setUp(): void\n    {\n        parent::setUp();\n\n        Crypt::swap(new Encrypter(self::TEST_CRYPT_KEY, 'AES-128-CBC'));\n    }\n\n    public function testEncryptableTrait()\n    {\n        $testModel = new TestModelEncryptable;\n\n        // Regular encrypt\n        $testModel->fill(['secret' => 'test']);\n        $this->assertEquals('test', $testModel->secret);\n        $this->assertNotEquals('test', $testModel->attributes['secret']);\n        $payloadOne = json_decode(base64_decode($testModel->attributes['secret']), true);\n        $this->assertEquals(['iv', 'value', 'mac', 'tag'], array_keys($payloadOne));\n\n        // Don't encrypt empty strings\n        $testModel->secret = '';\n        $this->assertEquals('', $testModel->secret);\n        $this->assertEquals('', $testModel->attributes['secret']);\n\n        // Encrypt numerics\n        $testModel->secret = 0;\n        $this->assertEquals(0, $testModel->secret);\n        $this->assertNotEquals(0, $testModel->attributes['secret']);\n        $payloadTwo = json_decode(base64_decode($testModel->attributes['secret']), true);\n        $this->assertEquals(['iv', 'value', 'mac', 'tag'], array_keys($payloadTwo));\n        $this->assertNotEquals($payloadOne['value'], $payloadTwo['value']);\n\n        // Test reset\n        $testModel->secret = null;\n        $this->assertNull($testModel->secret);\n        $this->assertNull($testModel->attributes['secret']);\n    }\n}\n\nclass TestModelEncryptable extends \\October\\Rain\\Database\\Model\n{\n    use \\October\\Rain\\Database\\Traits\\Encryptable;\n\n    protected $encryptable = ['secret'];\n    protected $fillable = ['secret'];\n    protected $table = 'secrets';\n}\n"
  },
  {
    "path": "tests/Database/Traits/SluggableTest.php",
    "content": "<?php\n\n/**\n * SluggableTest\n */\nclass SluggableTest extends TestCase\n{\n    /**\n     * setUp test\n     */\n    public function setUp(): void\n    {\n        $capsule = new Illuminate\\Database\\Capsule\\Manager;\n        $capsule->addConnection([\n            'driver' => 'sqlite',\n            'database' => ':memory:',\n            'prefix' => ''\n        ]);\n\n        // Create the dataset in the connection with the tables\n        $capsule->setAsGlobal();\n        $capsule->bootEloquent();\n\n        $capsule->schema()->create('test_sluggable', function ($table) {\n            $table->increments('id');\n            $table->string('name');\n            $table->string('slug')->unique();\n            $table->timestamps();\n        });\n\n        // Mock app instance for this test\n        App::swap(new class {\n            public function getLocale() { return 'en'; }\n        });\n    }\n\n    /**\n     * testSlugGeneration\n     */\n    public function testSlugGeneration()\n    {\n        $testModel1 = TestModelSluggable::create(['name' => 'test']);\n        $this->assertEquals($testModel1->slug, 'test');\n\n        $testModel2 = TestModelSluggable::create(['name' => 'test']);\n        $this->assertEquals($testModel2->slug, 'test-2');\n\n        $testModel3 = TestModelSluggable::create(['name' => 'test']);\n        $this->assertEquals($testModel3->slug, 'test-3');\n    }\n}\n\n/**\n * TestModelSluggable example class\n */\nclass TestModelSluggable extends Model\n{\n    use \\October\\Rain\\Database\\Traits\\Sluggable;\n\n    protected $slugs = ['slug' => 'name'];\n    protected $fillable = ['name'];\n    protected $table = 'test_sluggable';\n}\n"
  },
  {
    "path": "tests/Database/Traits/ValidationTest.php",
    "content": "<?php\n\nclass ValidationTest extends TestCase\n{\n    use \\October\\Rain\\Database\\Traits\\Validation;\n\n    public $exists;\n\n    public $id = 20;\n\n    public function testUniqueRule()\n    {\n        // Basic usage of unique rule\n        $rules = ['name' => 'unique', 'email' => 'unique:users'];\n\n        $this->exists = true;\n        $this->assertEquals([\n            'name' => ['unique:users,name,7,the_id'],\n            'email' => ['unique:users,email,7,the_id']\n        ], $this->processValidationRules($rules));\n\n        $this->exists = false;\n        $this->assertEquals([\n            'name' => ['unique:users'],\n            'email' => ['unique:users']\n        ], $this->processValidationRules($rules));\n\n        // Custom database connection\n        $rules = ['email' => 'unique:myconnection.users'];\n\n        $this->exists = true;\n        $this->assertEquals([\n            'email' => ['unique:myconnection.users,email,7,the_id']\n        ], $this->processValidationRules($rules));\n\n        $this->exists = false;\n        $this->assertEquals([\n            'email' => ['unique:myconnection.users']\n        ], $this->processValidationRules($rules));\n\n        // Custom table column name\n        $rules = ['email' => 'unique:users,email_address'];\n\n        $this->exists = true;\n        $this->assertEquals([\n            'email' => ['unique:users,email_address,7,the_id']\n        ], $this->processValidationRules($rules));\n\n        $this->exists = false;\n        $this->assertEquals([\n            'email' => ['unique:users,email_address']\n        ], $this->processValidationRules($rules));\n\n        // Forcing a unique rule to ignore a given ID\n        $rules = ['email' => 'unique:users,email_address,10'];\n\n        $this->exists = true;\n        $this->assertEquals([\n            'email' => ['unique:users,email_address,7,the_id']\n        ], $this->processValidationRules($rules));\n\n        $this->exists = false;\n        $this->assertEquals([\n            'email' => ['unique:users,email_address,10']\n        ], $this->processValidationRules($rules));\n\n        // Adding additional where clauses\n        $rules = ['email' => 'unique:users,email_address,NULL,id,account_id,1'];\n\n        $this->exists = true;\n        $this->assertEquals([\n            'email' => ['unique:users,email_address,20,id,account_id,1']\n        ], $this->processValidationRules($rules));\n\n        $this->exists = false;\n        $this->assertEquals([\n            'email' => ['unique:users,email_address,NULL,id,account_id,1']\n        ], $this->processValidationRules($rules));\n\n        // Adding multiple additional where clauses\n        $rules = ['email' => 'unique:users,email_address,NULL,id,account_id,1,account_name,\"Foo\",user_id,3'];\n\n        $this->exists = true;\n        $this->assertEquals([\n            'email' => ['unique:users,email_address,20,id,account_id,1,account_name,\"Foo\",user_id,3']\n        ], $this->processValidationRules($rules));\n\n        $this->exists = false;\n        $this->assertEquals([\n            'email' => ['unique:users,email_address,NULL,id,account_id,1,account_name,\"Foo\",user_id,3']\n        ], $this->processValidationRules($rules));\n    }\n\n    protected function getTable()\n    {\n        return 'users';\n    }\n\n    protected function getConnectionName()\n    {\n        return 'mysql';\n    }\n\n    protected function getKey()\n    {\n        return 7;\n    }\n\n    protected function getKeyName()\n    {\n        return 'the_id';\n    }\n\n    public function testArrayFieldNames()\n    {\n        $mock = $this->getMockForTrait(\\October\\Rain\\Database\\Traits\\Validation::class);\n\n        $rules = [\n            'field' => 'required',\n            'field.two' => 'required|boolean',\n            'field[three]' => 'required|date',\n            'field[three][child]' => 'required',\n            'field[four][][name]' => 'required',\n            'field[five' => 'required|string',\n            'field][six' => 'required|string',\n            'field]seven' => 'required|string',\n        ];\n        $rules = self::callProtectedMethod($mock, 'processRuleFieldNames', [$rules]);\n\n        $this->assertEquals([\n            'field' => 'required',\n            'field.two' => 'required|boolean',\n            'field.three' => 'required|date',\n            'field.three.child' => 'required',\n            'field.four.*.name' => 'required',\n            'field[five' => 'required|string',\n            'field][six' => 'required|string',\n            'field]seven' => 'required|string',\n        ], $rules);\n    }\n}\n"
  },
  {
    "path": "tests/Database/UpdaterTest.php",
    "content": "<?php\n\nuse October\\Rain\\Database\\Updater;\n\nclass UpdaterTest extends TestCase\n{\n    protected $updater;\n\n    public function setUp(): void\n    {\n        include_once __DIR__.'/../fixtures/database/SampleClass.php';\n\n        $this->updater = new Updater();\n    }\n\n    public function testClassNameGetsParsedCorrectly()\n    {\n        $reflector = new ReflectionClass(TestPlugin\\SampleClass::class);\n        $filePath = $reflector->getFileName();\n\n        $classFullName = $this->updater->getClassFromFile($filePath);\n\n        $this->assertEquals(TestPlugin\\SampleClass::class, $classFullName);\n    }\n}\n"
  },
  {
    "path": "tests/Events/EventDispatcherTest.php",
    "content": "<?php\n\nuse October\\Rain\\Events\\Dispatcher;\nuse October\\Rain\\Events\\PriorityDispatcher;\nuse October\\Rain\\Events\\FakeDispatcher;\n\n/**\n * EventDispatcherTest\n */\nclass EventDispatcherTest extends TestCase\n{\n    /**\n     * testFakerClass\n     */\n    public function testFakerClass(): void\n    {\n        $dispatcher = new PriorityDispatcher;\n\n        $dispatcher->setLaravelDispatcher(new Dispatcher);\n\n        Event::swap(new FakeDispatcher($dispatcher));\n\n        Event::fire(EventDispatcherTest::class);\n\n        Event::assertDispatched(EventDispatcherTest::class);\n    }\n}\n"
  },
  {
    "path": "tests/Extension/ExtendableTest.php",
    "content": "<?php\n\nuse October\\Rain\\Extension\\Extendable;\nuse October\\Rain\\Extension\\ExtensionBase;\n\nclass ExtendableTest extends TestCase\n{\n    public function testExtendingExtendableClass()\n    {\n        $subject = new ExtendableTestExampleExtendableClass;\n        $this->assertNull($subject->classAttribute);\n\n        ExtendableTestExampleExtendableClass::extend(function ($extension) {\n            $extension->classAttribute = 'bar';\n        });\n\n        $subject = new ExtendableTestExampleExtendableClass;\n        $this->assertEquals('bar', $subject->classAttribute);\n    }\n\n    public function testSettingDeclaredPropertyOnClass()\n    {\n        $subject = new ExtendableTestExampleExtendableClass;\n        $subject->classAttribute = 'Test';\n        $this->assertEquals('Test', $subject->classAttribute);\n    }\n\n    // public function testSettingUndeclaredPropertyOnClass()\n    // {\n    //     $this->expectException(\\BadMethodCallException::class);\n    //     $this->expectExceptionMessage(\"Call to undefined property ExtendableTestExampleExtendableClass::newAttribute\");\n\n    //     $subject = new ExtendableTestExampleExtendableClass;\n    //     $subject->newAttribute = 'Test';\n    // }\n\n    public function testSettingDeclaredPropertyOnBehavior()\n    {\n        $subject = new ExtendableTestExampleExtendableClass;\n        $behavior = $subject->getClassExtension('ExtendableTestExampleBehaviorClass1');\n\n        $subject->behaviorAttribute = 'Test';\n        $this->assertEquals('Test', $subject->behaviorAttribute);\n        $this->assertEquals('Test', $behavior->behaviorAttribute);\n        $this->assertTrue($subject->isClassExtendedWith('ExtendableTestExampleBehaviorClass1'));\n    }\n\n    public function testDynamicPropertyOnClass()\n    {\n        $subject = new ExtendableTestExampleExtendableClass;\n        $this->assertFalse($subject->propertyExists('newAttribute'));\n        $subject->addDynamicProperty('dynamicAttribute', 'Test');\n        $this->assertEquals('Test', $subject->dynamicAttribute);\n        $this->assertTrue($subject->propertyExists('dynamicAttribute'));\n    }\n\n    public function testDynamicallyImplementingClass()\n    {\n        ExtendableTestExampleImplementableClass::extend(function($obj) {\n            $obj->implementClassWith('ExtendableTestExampleBehaviorClass2');\n            $obj->implementClassWith('ExtendableTestExampleBehaviorClass2');\n            $obj->implementClassWith('ExtendableTestExampleBehaviorClass2');\n        });\n\n        $subject = new ExtendableTestExampleImplementableClass;\n        $this->assertTrue($subject->isClassExtendedWith('ExtendableTestExampleBehaviorClass1'));\n        $this->assertTrue($subject->isClassExtendedWith('ExtendableTestExampleBehaviorClass2'));\n    }\n\n    public function testDynamicallyExtendingClass()\n    {\n        $subject = new ExtendableTestExampleExtendableClass;\n        $subject->extendClassWith('ExtendableTestExampleBehaviorClass2');\n\n        $this->assertTrue($subject->isClassExtendedWith('ExtendableTestExampleBehaviorClass1'));\n        $this->assertTrue($subject->isClassExtendedWith('ExtendableTestExampleBehaviorClass2'));\n    }\n\n    public function testDynamicMethodOnClass()\n    {\n        $subject = new ExtendableTestExampleExtendableClass;\n        $subject->addDynamicMethod('getFooAnotherWay', 'getFoo', 'ExtendableTestExampleBehaviorClass1');\n\n        $this->assertEquals('foo', $subject->getFoo());\n        $this->assertEquals('foo', $subject->getFooAnotherWay());\n    }\n\n    public function testDynamicExtendAndMethodOnClass()\n    {\n        $subject = new ExtendableTestExampleExtendableClass;\n        $subject->extendClassWith('ExtendableTestExampleBehaviorClass2');\n        $subject->addDynamicMethod('getOriginalFoo', 'getFoo', 'ExtendableTestExampleBehaviorClass1');\n\n        $this->assertTrue($subject->isClassExtendedWith('ExtendableTestExampleBehaviorClass1'));\n        $this->assertTrue($subject->isClassExtendedWith('ExtendableTestExampleBehaviorClass2'));\n        $this->assertEquals('bar', $subject->getFoo());\n        $this->assertEquals('foo', $subject->getOriginalFoo());\n    }\n\n    public function testDynamicClosureOnClass()\n    {\n        $subject = new ExtendableTestExampleExtendableClass;\n        $subject->addDynamicMethod('sayHello', function () {\n            return 'Hello world';\n        });\n\n        $this->assertEquals('Hello world', $subject->sayHello());\n    }\n\n    public function testDynamicCallableOnClass()\n    {\n        $subject = new ExtendableTestExampleExtendableClass;\n        $subject->addDynamicMethod('getAppName', ['ExtendableTestExampleClass', 'getName']);\n\n        $this->assertEquals('october', $subject->getAppName());\n    }\n\n    public function testCallingStaticMethod()\n    {\n        $result = ExtendableTestExampleExtendableClass::getStaticBar();\n        $this->assertEquals('bar', $result);\n\n        $result = ExtendableTestExampleExtendableClass::vanillaIceIce();\n        $this->assertEquals('baby', $result);\n    }\n\n    public function testCallingUndefinedStaticMethod()\n    {\n        $this->expectException(BadMethodCallException::class);\n        $this->expectExceptionMessage('Call to undefined method ExtendableTestExampleExtendableClass::undefinedMethod()');\n\n        $result = ExtendableTestExampleExtendableClass::undefinedMethod();\n        $this->assertEquals('bar', $result);\n    }\n\n    // public function testAccessingProtectedProperty()\n    // {\n    //     $this->expectException(BadMethodCallException::class);\n    //     $this->expectExceptionMessage('Call to undefined property ExtendableTestExampleExtendableClass::protectedFoo');\n\n    //     $subject = new ExtendableTestExampleExtendableClass;\n    //     $this->assertEmpty($subject->protectedFoo);\n\n    //     $subject->protectedFoo = 'snickers';\n    //     $this->assertEquals('bar', $subject->getProtectedFooAttribute());\n    // }\n\n    public function testAccessingProtectedMethod()\n    {\n        $this->expectException(BadMethodCallException::class);\n        $this->expectExceptionMessage('Call to undefined method ExtendableTestExampleExtendableClass::protectedBar()');\n\n        $subject = new ExtendableTestExampleExtendableClass;\n        echo $subject->protectedBar();\n    }\n\n    public function testAccessingProtectedStaticMethod()\n    {\n        $this->expectException(BadMethodCallException::class);\n        $this->expectExceptionMessage('Call to undefined method ExtendableTestExampleExtendableClass::protectedMars()');\n\n        echo ExtendableTestExampleExtendableClass::protectedMars();\n    }\n\n    public function testInvalidImplementValue()\n    {\n        $this->expectException(Exception::class);\n        $this->expectExceptionMessage('Class ExtendableTestInvalidExtendableClass contains an invalid $implement value');\n\n        $result = new ExtendableTestInvalidExtendableClass;\n    }\n\n    public function testSoftImplementFake()\n    {\n        $result = new ExtendableTestExampleExtendableSoftImplementFakeClass;\n        $this->assertFalse($result->isClassExtendedWith('RabbleRabbleRabble'));\n        $this->assertEquals('working', $result->getStatus());\n    }\n\n    public function testSoftImplementReal()\n    {\n        $result = new ExtendableTestExampleExtendableSoftImplementRealClass;\n        $this->assertTrue($result->isClassExtendedWith('ExtendableTestExampleBehaviorClass1'));\n        $this->assertEquals('foo', $result->getFoo());\n    }\n\n    public function testSoftImplementCombo()\n    {\n        $result = new ExtendableTestExampleExtendableSoftImplementComboClass;\n        $this->assertFalse($result->isClassExtendedWith('RabbleRabbleRabble'));\n        $this->assertTrue($result->isClassExtendedWith('ExtendableTestExampleBehaviorClass1'));\n        $this->assertTrue($result->isClassExtendedWith('ExtendableTestExampleBehaviorClass2'));\n        $this->assertEquals('bar', $result->getFoo()); // ExtendableTestExampleBehaviorClass2 takes priority, defined last\n    }\n\n    public function testDotNotation()\n    {\n        $subject = new ExtendableTestExampleExtendableClassDotNotation();\n        $subject->extendClassWith('ExtendableTest.ExampleBehaviorClass2');\n\n        $this->assertTrue($subject->isClassExtendedWith('ExtendableTest.ExampleBehaviorClass1'));\n        $this->assertTrue($subject->isClassExtendedWith('ExtendableTest.ExampleBehaviorClass2'));\n    }\n\n    public function testMethodExists()\n    {\n        $subject = new ExtendableTestExampleExtendableClass;\n        $this->assertTrue($subject->methodExists('extend'));\n    }\n\n    public function testMethodNotExists()\n    {\n        $subject = new ExtendableTestExampleExtendableClass;\n        $this->assertFalse($subject->methodExists('missingFunction'));\n    }\n\n    public function testDynamicMethodExists()\n    {\n        $subject = new ExtendableTestExampleExtendableClass;\n        $subject->addDynamicMethod('getFooAnotherWay', 'getFoo', 'ExtendableTestExampleBehaviorClass1');\n\n        $this->assertTrue($subject->methodExists('getFooAnotherWay'));\n    }\n\n    public function testGetClassMethods()\n    {\n        $subject = new ExtendableTestExampleExtendableClass;\n        $subject->addDynamicMethod('getFooAnotherWay', 'getFoo', 'ExtendableTestExampleBehaviorClass1');\n        $methods = $subject->getClassMethods();\n\n        $this->assertContains('extend', $methods);\n        $this->assertContains('getFoo', $methods);\n        $this->assertContains('getFooAnotherWay', $methods);\n        $this->assertNotContains('missingFunction', $methods);\n    }\n\n    public function testIsInstanceOf()\n    {\n        $subject1 = new ExtendableTestExampleExtendableClass;\n        $subject2 = new ExtendableTestExampleExtendableSoftImplementFakeClass;\n        $subject3 = new ExtendableTestExampleExtendableSoftImplementRealClass;\n\n        $this->assertTrue($subject1->isClassInstanceOf(ExampleExtendableInterface::class));\n        $this->assertFalse($subject2->isClassInstanceOf(ExampleExtendableInterface::class));\n        $this->assertTrue($subject3->isClassInstanceOf(ExampleExtendableInterface::class));\n    }\n\n    public function testDynamicExtendOverridesMethod()\n    {\n        $subject = new ExtendableTestExampleExtendableClass;\n        $this->assertEquals('foo', $subject->getFoo());\n\n        $subject->extendClassWith('ExtendableTestExampleBehaviorClass2');\n        $this->assertEquals('bar', $subject->getFoo());\n    }\n\n    public function testDynamicExtendOverrideAsExtension()\n    {\n        $subject = new ExtendableTestExampleExtendableClass;\n        $subject->extendClassWith('ExtendableTestExampleBehaviorClass2');\n\n        // Default call resolves to the last registered behavior\n        $this->assertEquals('bar', $subject->getFoo());\n\n        // asExtension can still reach the original behavior\n        $this->assertEquals('foo', $subject->asExtension('ExtendableTestExampleBehaviorClass1')->getFoo());\n        $this->assertEquals('bar', $subject->asExtension('ExtendableTestExampleBehaviorClass2')->getFoo());\n    }\n\n    public function testMultipleBehaviorsMethodPriority()\n    {\n        $subject = new ExtendableTestExampleExtendableMultiBehaviorClass;\n\n        // BehaviorClass2 is last in $implement, so it wins\n        $this->assertEquals('bar', $subject->getFoo());\n\n        // Both behaviors are still accessible directly\n        $this->assertEquals('foo', $subject->asExtension('ExtendableTestExampleBehaviorClass1')->getFoo());\n        $this->assertEquals('bar', $subject->asExtension('ExtendableTestExampleBehaviorClass2')->getFoo());\n    }\n\n    public function testDynamicExtendOverridesImplementMethod()\n    {\n        $subject = new ExtendableTestExampleExtendableMultiBehaviorClass;\n        $this->assertEquals('bar', $subject->getFoo());\n\n        // Dynamic extension overrides both implemented behaviors\n        $subject->extendClassWith('ExtendableTestExampleBehaviorClass3');\n        $this->assertEquals('baz', $subject->getFoo());\n\n        // All three behaviors remain accessible via asExtension\n        $this->assertEquals('foo', $subject->asExtension('ExtendableTestExampleBehaviorClass1')->getFoo());\n        $this->assertEquals('bar', $subject->asExtension('ExtendableTestExampleBehaviorClass2')->getFoo());\n        $this->assertEquals('baz', $subject->asExtension('ExtendableTestExampleBehaviorClass3')->getFoo());\n    }\n}\n\n//\n// Test classes\n//\n\ninterface ExampleExtendableInterface\n{\n    public function hasPanda();\n}\n\n/**\n * Example behavior classes\n */\nclass ExtendableTestExampleBehaviorClass1 extends ExtensionBase\n{\n    public $behaviorAttribute;\n\n    public function getFoo()\n    {\n        return 'foo';\n    }\n\n    public static function getStaticBar()\n    {\n        return 'bar';\n    }\n\n    public static function vanillaIceIce()\n    {\n        return 'cream';\n    }\n\n    public function hasPanda()\n    {\n        return true;\n    }\n}\n\nclass ExtendableTestExampleBehaviorClass2 extends ExtensionBase\n{\n    public $behaviorAttribute;\n\n    public function getFoo()\n    {\n        return 'bar';\n    }\n}\n\n/*\n * Example class that has an invalid implementation\n */\nclass ExtendableTestInvalidExtendableClass extends Extendable\n{\n    public $implement = 24;\n\n    public $classAttribute;\n}\n\n/*\n * Example class that has extensions enabled\n */\nclass ExtendableTestExampleExtendableClass extends Extendable\n{\n    public $implement = ['ExtendableTestExampleBehaviorClass1'];\n\n    public $classAttribute;\n\n    protected $protectedFoo = 'bar';\n\n    public static function vanillaIceIce()\n    {\n        return 'baby';\n    }\n\n    protected function protectedBar()\n    {\n        return 'foo';\n    }\n\n    protected static function protectedMars()\n    {\n        return 'bar';\n    }\n\n    public function getProtectedFooAttribute()\n    {\n        return $this->protectedFoo;\n    }\n}\n\n/**\n * ExtendableTestExampleImplementableClass\n */\nclass ExtendableTestExampleImplementableClass extends Extendable\n{\n    public $implement = ['ExtendableTestExampleBehaviorClass1'];\n}\n\n/**\n * A normal class without extensions enabled\n */\nclass ExtendableTestExampleClass\n{\n    public static function getName()\n    {\n        return 'october';\n    }\n}\n\n/*\n * Example class with soft implement failure\n */\nclass ExtendableTestExampleExtendableSoftImplementFakeClass extends Extendable\n{\n    public $implement = ['@RabbleRabbleRabble'];\n\n    public static function getStatus()\n    {\n        return 'working';\n    }\n}\n\n/*\n * Example class with soft implement success\n */\nclass ExtendableTestExampleExtendableSoftImplementRealClass extends Extendable\n{\n    public $implement = ['@ExtendableTestExampleBehaviorClass1'];\n}\n\n/*\n * Example class with soft implement hybrid\n */\nclass ExtendableTestExampleExtendableSoftImplementComboClass extends Extendable\n{\n    public $implement = [\n        'ExtendableTestExampleBehaviorClass1',\n        '@ExtendableTestExampleBehaviorClass2',\n        '@RabbleRabbleRabble'\n    ];\n}\n\n/*\n * Example class that has extensions enabled using dot notation\n */\nclass ExtendableTestExampleExtendableClassDotNotation extends Extendable\n{\n    public $implement = ['ExtendableTest.ExampleBehaviorClass1'];\n\n    public $classAttribute;\n\n    protected $protectedFoo = 'bar';\n\n    public static function vanillaIceIce()\n    {\n        return 'baby';\n    }\n\n    protected function protectedBar()\n    {\n        return 'foo';\n    }\n\n    protected static function protectedMars()\n    {\n        return 'bar';\n    }\n\n    public function getProtectedFooAttribute()\n    {\n        return $this->protectedFoo;\n    }\n}\n\nclass ExtendableTestExampleBehaviorClass3 extends ExtensionBase\n{\n    public function getFoo()\n    {\n        return 'baz';\n    }\n}\n\n/*\n * Example class with multiple behaviors that define the same method\n */\nclass ExtendableTestExampleExtendableMultiBehaviorClass extends Extendable\n{\n    public $implement = [\n        'ExtendableTestExampleBehaviorClass1',\n        'ExtendableTestExampleBehaviorClass2',\n    ];\n}\n\n/*\n * Add namespaced aliases for dot notation test\n */\nclass_alias('ExtendableTestExampleBehaviorClass1', 'ExtendableTest\\\\ExampleBehaviorClass1');\nclass_alias('ExtendableTestExampleBehaviorClass2', 'ExtendableTest\\\\ExampleBehaviorClass2');\n"
  },
  {
    "path": "tests/Extension/ExtensionTest.php",
    "content": "<?php\n\nuse October\\Rain\\Extension\\Extendable;\nuse October\\Rain\\Extension\\ExtensionBase;\n\nclass ExtensionTest extends TestCase\n{\n    public function testExtendingBehavior()\n    {\n        $subject = new ExtensionTestExampleExtendableClass;\n        $this->assertEquals('foo', $subject->behaviorAttribute);\n\n        ExtensionTestExampleBehaviorClass1::extend(function ($extension) {\n            $extension->behaviorAttribute = 'bar';\n        });\n\n        $subject = new ExtensionTestExampleExtendableClass;\n        $this->assertEquals('bar', $subject->behaviorAttribute);\n    }\n}\n\n/*\n * Example class that has extensions enabled\n */\nclass ExtensionTestExampleExtendableClass extends Extendable\n{\n    public $implement = ['ExtensionTestExampleBehaviorClass1'];\n}\n\n/**\n * Example behavior classes\n */\nclass ExtensionTestExampleBehaviorClass1 extends ExtensionBase\n{\n    public $behaviorAttribute = 'foo';\n}\n"
  },
  {
    "path": "tests/Halcyon/DatasourceResolverTest.php",
    "content": "<?php\n\nuse October\\Rain\\Filesystem\\Filesystem;\nuse October\\Rain\\Halcyon\\Datasource\\Resolver;\nuse October\\Rain\\Halcyon\\Datasource\\FileDatasource;\n\nclass DatasourceResolverTest extends TestCase\n{\n    public function testConstruct()\n    {\n        $theme1 = new FileDatasource('themes/theme1', new Filesystem);\n        $theme2 = new FileDatasource('themes/theme2', new Filesystem);\n        $theme3 = new FileDatasource('themes/theme3', new Filesystem);\n\n        $resolver = new Resolver([\n            'theme1' => $theme1,\n            'theme2' => $theme2,\n            'theme3' => $theme3\n        ]);\n\n        $this->assertTrue($resolver->hasDatasource('theme1'));\n        $this->assertTrue($resolver->hasDatasource('theme2'));\n        $this->assertTrue($resolver->hasDatasource('theme3'));\n        $this->assertFalse($resolver->hasDatasource('theme4'));\n    }\n\n    public function testDefaultDatasource()\n    {\n        $resolver = new Resolver;\n        $resolver->setDefaultDatasource('theme1');\n        $this->assertEquals('theme1', $resolver->getDefaultDatasource());\n    }\n}\n"
  },
  {
    "path": "tests/Halcyon/HalcyonModelTest.php",
    "content": "<?php\n\nuse October\\Rain\\Halcyon\\Model;\nuse October\\Rain\\Halcyon\\Datasource\\Resolver;\nuse October\\Rain\\Halcyon\\Datasource\\FileDatasource;\nuse October\\Rain\\Filesystem\\Filesystem;\n\nclass HalcyonModelTest extends TestCase\n{\n    protected $resolver;\n\n    public function setUp(): void\n    {\n        include_once __DIR__.'/../fixtures/halcyon/models/Page.php';\n        include_once __DIR__.'/../fixtures/halcyon/models/Menu.php';\n        include_once __DIR__.'/../fixtures/halcyon/models/Content.php';\n\n        $this->setDatasourceResolver();\n\n        $this->setValidatorOnModel();\n    }\n\n    public function testFindAll()\n    {\n        $pages = HalcyonTestPage::all();\n\n        $this->assertCount(4, $pages);\n        $this->assertContains('about.htm', $pages->lists('fileName'));\n        $this->assertContains('home.htm', $pages->lists('fileName'));\n        $this->assertContains('level1/team.htm', $pages->lists('fileName'));\n        $this->assertContains('level1/level2/level3/level4/level5/contact.htm', $pages->lists('fileName'));\n    }\n\n    public function testFindPage()\n    {\n        $page = HalcyonTestPage::find('home');\n        $this->assertNotNull($page);\n        $this->assertCount(6, $page->attributes);\n        $this->assertArrayHasKey('fileName', $page->attributes);\n        $this->assertEquals('home.htm', $page->fileName);\n        $this->assertCount(1, $page->settings);\n        $this->assertEquals('<h1>World!</h1>', $page->markup);\n        $this->assertEquals('hello', $page->title);\n    }\n\n    public function testFindMenu()\n    {\n        $menu = HalcyonTestMenu::find('mainmenu');\n        $this->assertNotNull($menu);\n        $this->assertEquals('<ul><li>Home</li></ul>', $menu->content);\n    }\n\n    public function testOtherDatasourcePage()\n    {\n        $page = HalcyonTestPage::on('theme2')->find('home');\n        $this->assertNotNull($page);\n        $this->assertCount(6, $page->attributes);\n        $this->assertArrayHasKey('fileName', $page->attributes);\n        $this->assertEquals('home.htm', $page->fileName);\n        $this->assertCount(1, $page->settings);\n        $this->assertEquals('<h1>Chisel</h1>', $page->markup);\n        $this->assertEquals('Cold', $page->title);\n    }\n\n    public function testCreatePage()\n    {\n        @unlink($targetFile = __DIR__.'/../fixtures/halcyon/themes/theme1/pages/testfile.htm');\n\n        HalcyonTestPage::create([\n            'fileName' => 'testfile.htm',\n            'title' => 'Test page',\n            'viewBag' => ['foo' => 'bar'],\n            'markup' => '<p>Hello world!</p>',\n            'code' => 'function onStart() { }'\n        ]);\n\n        $this->assertFileExists($targetFile);\n\n        $content = <<<ESC\ntitle = \"Test page\"\n\n[viewBag]\nfoo = \"bar\"\n==\n<?php\nfunction onStart() { }\n?>\n==\n<p>Hello world!</p>\nESC;\n\n        $expected = file_get_contents($targetFile);\n        $expected = preg_replace('~\\R~u', PHP_EOL, $expected); // Normalize EOL\n        $content = preg_replace('~\\R~u', PHP_EOL, $content); // Normalize EOL\n        $this->assertEquals($content, $expected);\n\n        @unlink($targetFile);\n    }\n\n    public function testCreatePageMarkupSections()\n    {\n        @unlink($targetFile = __DIR__.'/../fixtures/halcyon/themes/theme1/pages/testfile.htm');\n\n        $markup = <<< ESC\nfunction onStart() { }\n==\n<p>Hello world!</p>\nESC;\n\n        HalcyonTestPage::create([\n            'fileName' => 'testfile.htm',\n            'title' => 'Test page',\n            'markup' => $markup\n        ]);\n\n        $this->assertFileExists($targetFile);\n\n        $content = <<<ESC\ntitle = \"Test page\"\n==\nfunction onStart() { }\n\n<p>Hello world!</p>\nESC;\n\n        $expected = file_get_contents($targetFile);\n        $expected = preg_replace('~\\R~u', PHP_EOL, $expected); // Normalize EOL\n        $content = preg_replace('~\\R~u', PHP_EOL, $content); // Normalize EOL\n        $this->assertEquals($content, $expected);\n\n        @unlink($targetFile);\n    }\n\n    public function testCreateMenu()\n    {\n        @unlink($targetFile = __DIR__.'/../fixtures/halcyon/themes/theme1/menus/testfile.htm');\n\n        HalcyonTestMenu::create([\n            'fileName' => 'testfile',\n            'content' => '<p>Hello world!</p>'\n        ]);\n\n        $this->assertFileExists($targetFile);\n\n        $content = <<<ESC\n<p>Hello world!</p>\nESC;\n\n        $this->assertEquals($content, file_get_contents($targetFile));\n\n        @unlink($targetFile);\n    }\n\n    public function testCreatePageInDirectoryPass()\n    {\n        @unlink($targetFile = __DIR__.'/../fixtures/halcyon/themes/theme1/pages/walking/on-sunshine.htm');\n\n        HalcyonTestPage::create([\n            'fileName' => 'walking/on-sunshine.htm',\n            'title' => 'Katrina & The Waves',\n            'markup' => '<p>Woo!</p>',\n        ]);\n\n        $this->assertFileExists($targetFile);\n\n        @unlink($targetFile);\n        @rmdir(dirname($targetFile));\n    }\n\n    public function testCreatePageInDirectoryFail()\n    {\n        $this->expectException(\\October\\Rain\\Halcyon\\Exception\\InvalidFileNameException::class);\n        $this->expectExceptionMessage('The specified file name [one/small/step/for-man.htm] is invalid.');\n\n        HalcyonTestPage::create([\n            'fileName' => 'one/small/step/for-man.htm',\n            'title' => 'One Giant Leap',\n            'markup' => '<p>For man-kind</p>',\n        ]);\n    }\n\n    public function testUpdatePage()\n    {\n        @unlink($targetFile = __DIR__.'/../fixtures/halcyon/themes/theme1/pages/testfile2.htm');\n\n        try {\n            $page = HalcyonTestPage::create([\n                'fileName' => 'testfile2',\n                'title' => 'Another test',\n                'markup' => '<p>Foo bar!</p>'\n            ]);\n\n            $this->assertFileExists($targetFile);\n            $this->assertEquals('Another test', $page->title);\n\n            $page = HalcyonTestPage::find('testfile2');\n            $this->assertEquals('Another test', $page->title);\n            $page->title = 'All done!';\n            $page->save();\n\n            $page = HalcyonTestPage::find('testfile2');\n            $this->assertEquals('All done!', $page->title);\n\n            $page->update(['title' => 'Try this']);\n            $page = HalcyonTestPage::find('testfile2');\n            $this->assertEquals('Try this', $page->title);\n\n            $this->assertFalse($page->isDirty());\n            unset($page->title);\n            $this->assertTrue($page->isDirty());\n        }\n        finally {\n            @unlink($targetFile = __DIR__.'/../fixtures/halcyon/themes/theme1/pages/testfile2.htm');\n        }\n    }\n\n    public function testUpdatePageRenameFile()\n    {\n        @unlink($targetFile = __DIR__.'/../fixtures/halcyon/themes/theme1/pages/testfile2.htm');\n\n        try {\n            $page = HalcyonTestPage::create([\n                'fileName' => 'testfile2',\n                'title' => 'Another test',\n                'markup' => '<p>Foo bar!</p>'\n            ]);\n\n            $this->assertFileExists($targetFile);\n\n            $page->fileName = 'renamedtest1';\n            $page->save();\n\n            $newTargetFile = __DIR__.'/../fixtures/halcyon/themes/theme1/pages/renamedtest1.htm';\n            $this->assertFileNotExists($targetFile);\n            $this->assertFileExists($newTargetFile);\n        }\n        finally {\n            @unlink($newTargetFile);\n        }\n    }\n\n    public function testUpdatePageRenameFileCase()\n    {\n        @unlink($targetFile = __DIR__.'/../fixtures/halcyon/themes/theme1/pages/Test.htm');\n\n        try {\n            $page = HalcyonTestPage::create([\n                'fileName' => 'Test',\n                'title' => 'Upper case file',\n                'markup' => '<p>I have an upper case, it should be lower</p>'\n            ]);\n\n            $this->assertFileExists($targetFile);\n\n            $page->fileName = 'test';\n            $page->save();\n\n            $newTargetFile = __DIR__.'/../fixtures/halcyon/themes/theme1/pages/test.htm';\n            $this->assertFileExists($newTargetFile);\n        }\n        finally {\n            @unlink($newTargetFile);\n        }\n    }\n\n    public function testUpdateContentRenameExtension()\n    {\n        $content = HalcyonTestContent::find('welcome.htm');\n        $this->assertNotNull($content);\n        $this->assertCount(5, $content->attributes);\n        $this->assertArrayHasKey('fileName', $content->attributes);\n        $this->assertEquals('welcome.htm', $content->fileName);\n        $this->assertEquals('<p>Hi friend</p>', $content->markup);\n\n        $targetFile = __DIR__.'/../fixtures/halcyon/themes/theme1/content/welcome.htm';\n        $newTargetFile = __DIR__.'/../fixtures/halcyon/themes/theme1/content/welcome.txt';\n\n        $this->assertFileExists($targetFile);\n\n        $content->fileName = 'welcome.txt';\n        $content->save();\n\n        $this->assertFileExists($newTargetFile);\n        $this->assertFileNotExists($targetFile);\n\n        $content->fileName = 'welcome.htm';\n        $content->save();\n\n        $this->assertFileNotExists($newTargetFile);\n        $this->assertFileExists($targetFile);\n    }\n\n    public function testUpdatePageFileExists()\n    {\n        $this->expectException(\\October\\Rain\\Halcyon\\Exception\\FileExistsException::class);\n        $this->expectExceptionMessage('A file already exists');\n\n        @unlink($targetFile = __DIR__.'/../fixtures/halcyon/themes/theme1/pages/testfile2a.htm');\n\n        $page = HalcyonTestPage::create([\n            'fileName' => 'testfile2a',\n            'title' => 'Another test',\n            'markup' => '<p>Foo bar!</p>'\n        ]);\n\n        $this->assertFileExists($targetFile);\n        $this->assertEquals('Another test', $page->title);\n\n        $page = HalcyonTestPage::find('testfile2a');\n        $page->fileName = 'about';\n\n        @unlink($targetFile);\n\n        $page->save();\n    }\n\n    public function testDeletePage()\n    {\n        @unlink($targetFile = __DIR__.'/../fixtures/halcyon/themes/theme1/pages/testfile3.htm');\n\n        $page = HalcyonTestPage::create([\n            'fileName' => 'testfile3',\n            'title' => 'To be deleted',\n        ]);\n\n        $this->assertFileExists($targetFile);\n\n        $page->delete();\n\n        $this->assertFileNotExists($targetFile);\n    }\n\n    public function testPageWithValidation()\n    {\n        $this->expectException(\\October\\Rain\\Halcyon\\Exception\\ModelException::class);\n        $this->expectExceptionMessage('The title field is required.');\n\n        $page = new HalcyonTestPageWithValidation;\n        $page->fileName = 'with-validation';\n        $page->save();\n\n        $page->delete();\n    }\n\n    public function testPageWithNestedValidationFail()\n    {\n        $this->expectException(\\October\\Rain\\Halcyon\\Exception\\ModelException::class);\n        $this->expectExceptionMessage('The meta title field is required.');\n\n        $page = new HalcyonTestPageWithValidation;\n        $page->fileName = 'with-validation';\n        $page->title = \"Pass\";\n        $page->save();\n\n        $page->delete();\n    }\n\n    public function testPageWithNestedValidationPass()\n    {\n        $this->expectNotToPerformAssertions();\n\n        $page = new HalcyonTestPageWithValidation;\n        $page->fileName = 'with-validation';\n        $page->title = \"Pass\";\n        $page->viewBag = ['meta_title' => 'Oh yeah'];\n        $page->save();\n\n        $page->delete();\n    }\n\n    public function testPageQueryListFileName()\n    {\n        $page = new HalcyonTestPageWithValidation;\n        $files = $page->newQuery()->lists('fileName');\n        sort($files);\n\n        $this->assertCount(4, $files);\n        $this->assertEquals([\n            'about.htm',\n            'home.htm',\n            'level1/level2/level3/level4/level5/contact.htm',\n            'level1/team.htm'\n        ], $files);\n    }\n\n    public function testAddDynamicProperty()\n    {\n        @unlink($targetFile = __DIR__.'/../fixtures/halcyon/themes/theme1/pages/dynamicproperty.htm');\n\n        try {\n            $page = HalcyonTestPage::create([\n                'fileName' => 'dynamicproperty',\n                'title' => 'Add Dynamic Property',\n                'markup' => '<p>Foo bar!</p>'\n            ]);\n\n            $page->addDynamicProperty('myDynamicProperty', 'myDynamicPropertyValue');\n\n            // Dynamic property should not hit attributes\n            $this->assertArrayNotHasKey('myDynamicProperty', $page->attributes);\n\n            // Should be a real property\n            $this->assertTrue($page->propertyExists('myDynamicProperty'));\n            $this->assertEquals('myDynamicPropertyValue', $page->myDynamicProperty);\n\n            $page->save();\n\n            // Should not leak in to attributes\n            $page = HalcyonTestPage::find('dynamicproperty');\n            $this->assertNotNull($page);\n            $this->assertArrayNotHasKey('myDynamicProperty', $page->attributes);\n            $this->assertFalse($page->propertyExists('myDynamicProperty'));\n        }\n        finally {\n            @unlink($targetFile);\n        }\n    }\n\n    public function testAddBehaviorClass()\n    {\n        $page = new HalcyonTestPage;\n        $page->extendClassWith(HalyconTestExampleBehaviorClass::class);\n\n        $this->assertEquals(null, $page->protectedFoo);\n        $this->assertEquals('bar', $page->getFoo());\n\n        // @todo Halycon not advanced enough for this yet\n        // $this->assertEquals('foobar', $page->behaviorAttribute);\n    }\n\n    //\n    // House keeping\n    //\n\n    protected function setDatasourceResolver()\n    {\n        $theme1 = new FileDatasource(realpath(__DIR__.'/../fixtures/halcyon/themes/theme1'), new Filesystem);\n        $this->resolver = new Resolver(['theme1' => $theme1]);\n        $this->resolver->setDefaultDatasource('theme1');\n\n        $theme2 = new FileDatasource(realpath(__DIR__.'/../fixtures/halcyon/themes/theme2'), new Filesystem);\n        $this->resolver->addDatasource('theme2', $theme2);\n\n        Model::setDatasourceResolver($this->resolver);\n    }\n\n    protected function setValidatorOnModel()\n    {\n        $translator = $this->getMockBuilder('Illuminate\\Contracts\\Translation\\Translator')->setMethods([\n            'get',\n            'choice',\n            'trans',\n            'transChoice',\n            'setLocale',\n            'getLocale'\n        ])->getMock();\n\n        $translator->expects($this->any())->method('get')->will($this->returnArgument(0));\n\n        $factory = new \\Illuminate\\Validation\\Factory($translator);\n\n        HalcyonTestPageWithValidation::setModelValidator($factory);\n    }\n}\n\nclass HalyconTestExampleBehaviorClass extends October\\Rain\\Extension\\ExtensionBase\n{\n    public $behaviorAttribute = 'foobar';\n\n    protected $protectedFoo = 'bar';\n\n    public function getFoo()\n    {\n        return 'bar';\n    }\n}\n"
  },
  {
    "path": "tests/Halcyon/SectionParserTest.php",
    "content": "<?php\n\nuse October\\Rain\\Halcyon\\Processors\\SectionParser;\n\nclass SectionParserTest extends TestCase\n{\n    public function testParse()\n    {\n        // Test a single section\n        $result = SectionParser::parse(\"this is a twig content\");\n        $this->assertIsArray($result);\n        $this->assertCount(3, $result);\n        $this->assertArrayHasKey(\"settings\", $result);\n        $this->assertArrayHasKey(\"code\", $result);\n        $this->assertArrayHasKey(\"markup\", $result);\n        $this->assertEmpty($result[\"settings\"]);\n        $this->assertNull($result[\"code\"]);\n        $this->assertNotNull($result[\"markup\"]);\n        $this->assertEquals(\"this is a twig content\", $result[\"markup\"]);\n\n        // Test two sections\n        $result = SectionParser::parse(\"url = \\\"/blog/post/\\\" \\n==\\n this is a twig content\");\n        $this->assertIsArray($result);\n        $this->assertCount(3, $result);\n        $this->assertArrayHasKey(\"settings\", $result);\n        $this->assertArrayHasKey(\"code\", $result);\n        $this->assertArrayHasKey(\"markup\", $result);\n        $this->assertNotNull($result[\"markup\"]);\n        $this->assertNotNull($result[\"settings\"]);\n        $this->assertNull($result[\"code\"]);\n        $this->assertEquals(\"this is a twig content\", $result[\"markup\"]);\n        $this->assertIsArray($result[\"settings\"]);\n        $this->assertArrayHasKey(\"url\", $result[\"settings\"]);\n        $this->assertEquals(\"/blog/post/\", $result[\"settings\"][\"url\"]);\n\n        // Test three sections\n        $result = SectionParser::parse(\"url = \\\"/blog/post/\\\"\\n[section]\\nindex = value \\n==\\n \\$var = 23; \\n phpinfo(); \\n==\\n this is a twig content\");\n        $this->assertIsArray($result);\n        $this->assertCount(3, $result);\n        $this->assertArrayHasKey(\"settings\", $result);\n        $this->assertArrayHasKey(\"code\", $result);\n        $this->assertArrayHasKey(\"markup\", $result);\n        $this->assertNotNull($result[\"settings\"]);\n        $this->assertNotNull($result[\"markup\"]);\n        $this->assertNotNull($result[\"code\"]);\n        $this->assertEquals(\"this is a twig content\", $result[\"markup\"]);\n        $this->assertIsArray($result[\"settings\"]);\n        $this->assertArrayHasKey(\"url\", $result[\"settings\"]);\n        $this->assertEquals(\"/blog/post/\", $result[\"settings\"][\"url\"]);\n        $this->assertStringContainsString(\"\\$var = 23;\", $result[\"code\"]);\n        $this->assertStringContainsString(\"phpinfo();\", $result[\"code\"]);\n\n        $this->assertArrayHasKey(\"section\", $result[\"settings\"]);\n        $this->assertIsArray($result[\"settings\"][\"section\"]);\n        $this->assertArrayHasKey(\"index\", $result[\"settings\"][\"section\"]);\n        $this->assertEquals(\"value\", $result[\"settings\"][\"section\"][\"index\"]);\n\n        // Test zero sections\n        $result = SectionParser::parse(\"\");\n        $this->assertCount(3, $result);\n        $this->assertArrayHasKey(\"settings\", $result);\n        $this->assertArrayHasKey(\"code\", $result);\n        $this->assertArrayHasKey(\"markup\", $result);\n        $this->assertEmpty($result[\"settings\"]);\n        $this->assertNull($result[\"markup\"]);\n        $this->assertNull($result[\"code\"]);\n        $this->assertEquals(\"\", $result[\"markup\"]);\n\n        // Test doesn't break Markdown single section\n        $result = SectionParser::parse(\"This is a header\\n================\\n\\nThis is a paragraph\");\n        $this->assertCount(3, $result);\n        $this->assertArrayHasKey(\"settings\", $result);\n        $this->assertArrayHasKey(\"code\", $result);\n        $this->assertArrayHasKey(\"markup\", $result);\n        $this->assertEmpty($result[\"settings\"]);\n        $this->assertNull($result[\"code\"]);\n        $this->assertNotNull($result[\"markup\"]);\n        $this->assertEquals(\"This is a header\\n================\\n\\nThis is a paragraph\", $result[\"markup\"]);\n\n        // Test doesn't break Markdown two sections\n        $result = SectionParser::parse(\"url = \\\"/blog/post\\\"\\n==\\nThis is a header\\n================\\n\\nThis is a paragraph\");\n        $this->assertCount(3, $result);\n        $this->assertArrayHasKey(\"settings\", $result);\n        $this->assertArrayHasKey(\"code\", $result);\n        $this->assertArrayHasKey(\"markup\", $result);\n        $this->assertNotNull($result[\"settings\"]);\n        $this->assertArrayHasKey(\"url\", $result[\"settings\"]);\n        $this->assertEquals(\"/blog/post\", $result[\"settings\"][\"url\"]);\n        $this->assertNull($result[\"code\"]);\n        $this->assertNotNull($result[\"markup\"]);\n        $this->assertEquals(\"This is a header\\n================\\n\\nThis is a paragraph\", $result[\"markup\"]);\n\n        // Test doesn't break Markdown three sections\n        $result = SectionParser::parse(\"url = \\\"/blog/post\\\"\\n==\\n\\$var = 23; \\n phpinfo();\\n==\\nThis is a header\\n================\\n\\nThis is a paragraph\");\n        $this->assertCount(3, $result);\n        $this->assertArrayHasKey(\"settings\", $result);\n        $this->assertArrayHasKey(\"code\", $result);\n        $this->assertArrayHasKey(\"markup\", $result);\n        $this->assertNotNull($result[\"settings\"]);\n        $this->assertArrayHasKey(\"url\", $result[\"settings\"]);\n        $this->assertEquals(\"/blog/post\", $result[\"settings\"][\"url\"]);\n        $this->assertNotNull($result[\"code\"]);\n        $this->assertStringContainsString(\"\\$var = 23;\", $result[\"code\"]);\n        $this->assertStringContainsString(\"phpinfo();\", $result[\"code\"]);\n        $this->assertNotNull($result[\"markup\"]);\n        $this->assertEquals(\"This is a header\\n================\\n\\nThis is a paragraph\", $result[\"markup\"]);\n    }\n\n    public function testParseOffset()\n    {\n\n        // Test three sections\n        $content = <<<ESC\nsetting = \"test\"\n==\nfunction onStart() { // Line 3\n\n}\n==\n<p>Line 7</p>\nESC;\n\n        $result = SectionParser::parseOffset($content);\n        $this->assertArrayHasKey(\"settings\", $result);\n        $this->assertArrayHasKey(\"code\", $result);\n        $this->assertArrayHasKey(\"markup\", $result);\n        $this->assertNotNull($result[\"settings\"]);\n        $this->assertNotNull($result[\"code\"]);\n        $this->assertNotNull($result[\"markup\"]);\n        $this->assertEquals(1, $result[\"settings\"]);\n        $this->assertEquals(3, $result[\"code\"]);\n        $this->assertEquals(7, $result[\"markup\"]);\n\n        // Test two sections\n        $content = <<<ESC\nsetting = \"test\"\nanother = \"setting\"\nfoo = \"bar\"\n==\n<p>Line 5</p>\nESC;\n        $result = SectionParser::parseOffset($content);\n        $this->assertArrayHasKey(\"settings\", $result);\n        $this->assertArrayHasKey(\"code\", $result);\n        $this->assertArrayHasKey(\"markup\", $result);\n        $this->assertNotNull($result[\"markup\"]);\n        $this->assertNotNull($result[\"settings\"]);\n        $this->assertNull($result[\"code\"]);\n        $this->assertEquals(1, $result[\"settings\"]);\n        $this->assertEquals(5, $result[\"markup\"]);\n\n        // Test two sections with white space\n        $content = <<<ESC\n\n\nline = \"Line 3\"\nanother = \"setting\"\nfoo = \"bar\"\n==\n\n\n\n\n\n<p>Line 12</p>\nESC;\n        $result = SectionParser::parseOffset($content);\n        $this->assertArrayHasKey(\"settings\", $result);\n        $this->assertArrayHasKey(\"code\", $result);\n        $this->assertArrayHasKey(\"markup\", $result);\n        $this->assertNotNull($result[\"markup\"]);\n        $this->assertNotNull($result[\"settings\"]);\n        $this->assertNull($result[\"code\"]);\n        $this->assertEquals(3, $result[\"settings\"]);\n        $this->assertEquals(12, $result[\"markup\"]);\n\n        // Test one section\n        $content = <<<ESC\n<p>Line 1</p>\n<p>Line 2</p>\n<p>Line 3</p>\nESC;\n        $result = SectionParser::parseOffset($content);\n        $this->assertArrayHasKey(\"settings\", $result);\n        $this->assertArrayHasKey(\"code\", $result);\n        $this->assertArrayHasKey(\"markup\", $result);\n        $this->assertNotNull($result[\"markup\"]);\n        $this->assertNull($result[\"settings\"]);\n        $this->assertNull($result[\"code\"]);\n        $this->assertEquals(1, $result[\"markup\"]);\n\n\n        // Test empty PHP\n        $content = <<<ESC\nsetting = \"test\"\n==\n==\n<p>Line 4</p>\nESC;\n\n        $result = SectionParser::parseOffset($content);\n        $this->assertArrayHasKey(\"settings\", $result);\n        $this->assertArrayHasKey(\"code\", $result);\n        $this->assertArrayHasKey(\"markup\", $result);\n        $this->assertNotNull($result[\"settings\"]);\n        $this->assertNotNull($result[\"code\"]);\n        $this->assertNotNull($result[\"markup\"]);\n        $this->assertEquals(1, $result[\"settings\"]);\n        $this->assertEquals(3, $result[\"code\"]);\n        $this->assertEquals(4, $result[\"markup\"]);\n\n        // Test with PHP tags\n        $content = <<<ESC\nsetting = \"test\"\nanother = \"setting\"\n==\n<?\nfunction onStart() {\n\n}\n?>\n==\n<p>Line 10</p>\nESC;\n\n        $result = SectionParser::parseOffset($content);\n        $this->assertArrayHasKey(\"settings\", $result);\n        $this->assertArrayHasKey(\"code\", $result);\n        $this->assertArrayHasKey(\"markup\", $result);\n        $this->assertNotNull($result[\"settings\"]);\n        $this->assertNotNull($result[\"code\"]);\n        $this->assertNotNull($result[\"markup\"]);\n        $this->assertEquals(1, $result[\"settings\"]);\n        $this->assertEquals(5, $result[\"code\"]);\n        $this->assertEquals(10, $result[\"markup\"]);\n\n        // Test with PHP tags and whitespace\n        $content = <<<ESC\nsetting = \"test\"\nanother = \"setting\"\nfoo = \"bar\"\n==\n\n\n\n\n\n\n\n<?php\nfunction onStart() { // Line 13\n\n}\n?>\n==\n<p>Line 18</p>\nESC;\n\n        $result = SectionParser::parseOffset($content);\n        $this->assertArrayHasKey(\"settings\", $result);\n        $this->assertArrayHasKey(\"code\", $result);\n        $this->assertArrayHasKey(\"markup\", $result);\n        $this->assertNotNull($result[\"settings\"]);\n        $this->assertNotNull($result[\"code\"]);\n        $this->assertNotNull($result[\"markup\"]);\n        $this->assertEquals(1, $result[\"settings\"]);\n        $this->assertEquals(13, $result[\"code\"]);\n        $this->assertEquals(18, $result[\"markup\"]);\n\n        // Test with PHP tags and whitespace both sides\n        $content = <<<ESC\nsetting = \"test\"\nanother = \"setting\"\nfoo = \"bar\"\n==\n\n\n\n\n\n\n\n<?php\n\n\n\n\n\n\n\nfunction onStart() { // Line 20\n\n}\n?>\n==\n<p>Line 25</p>\nESC;\n\n        $result = SectionParser::parseOffset($content);\n        $this->assertArrayHasKey(\"settings\", $result);\n        $this->assertArrayHasKey(\"code\", $result);\n        $this->assertArrayHasKey(\"markup\", $result);\n        $this->assertNotNull($result[\"settings\"]);\n        $this->assertNotNull($result[\"code\"]);\n        $this->assertNotNull($result[\"markup\"]);\n        $this->assertEquals(1, $result[\"settings\"]);\n        $this->assertEquals(20, $result[\"code\"]);\n        $this->assertEquals(25, $result[\"markup\"]);\n\n        // Test with whitespace on PHP and Twig\n        $content = <<<ESC\nsetting = \"test\"\nanother = \"setting\"\nfoo = \"bar\"\n==\n\n\n\nfunction onStart() { // Line 8\n\n}\n==\n\n\n\n\n\n\n\n<p>Line 19</p>\nESC;\n\n        $result = SectionParser::parseOffset($content);\n        $this->assertArrayHasKey(\"settings\", $result);\n        $this->assertArrayHasKey(\"code\", $result);\n        $this->assertArrayHasKey(\"markup\", $result);\n        $this->assertNotNull($result[\"settings\"]);\n        $this->assertNotNull($result[\"code\"]);\n        $this->assertNotNull($result[\"markup\"]);\n        $this->assertEquals(1, $result[\"settings\"]);\n        $this->assertEquals(8, $result[\"code\"]);\n        $this->assertEquals(19, $result[\"markup\"]);\n\n        // Test namespaces\n        $content = <<<ESC\n\nline = \"Line 2\"\nsetting = \"test\"\n==\n\nuse October\\Rain\\Support\\Str; // This will be removed (-1 line)\nuse October\\Rain\\Flash\\FlashBag; // This will be removed (-1 line)\n\nfunction onStart() { // Line 7\n\n    use October\\Rain\\Support\\Str; // And placed here\n    use October\\Rain\\Flash\\FlashBag; // And placed here\n\n}\n==\n<p>Line 16</p>\nESC;\n\n        $result = SectionParser::parseOffset($content);\n        $this->assertArrayHasKey(\"settings\", $result);\n        $this->assertArrayHasKey(\"code\", $result);\n        $this->assertArrayHasKey(\"markup\", $result);\n        $this->assertNotNull($result[\"settings\"]);\n        $this->assertNotNull($result[\"code\"]);\n        $this->assertNotNull($result[\"markup\"]);\n        $this->assertEquals(2, $result[\"settings\"]);\n        $this->assertEquals(7, $result[\"code\"]);\n        $this->assertEquals(16, $result[\"markup\"]);\n    }\n}\n"
  },
  {
    "path": "tests/Halcyon/ValidationTraitTest.php",
    "content": "<?php\n\nclass ValidationTraitTest extends TestCase\n{\n    public function testArrayFieldNames()\n    {\n        $mock = $this->getMockForTrait('October\\Rain\\Halcyon\\Traits\\Validation');\n\n        $rules = [\n            'field' => 'required',\n            'field.two' => 'required|boolean',\n            'field[three]' => 'required|date',\n            'field[three][child]' => 'required',\n            'field[four][][name]' => 'required',\n            'field[five' => 'required|string',\n            'field][six' => 'required|string',\n            'field]seven' => 'required|string',\n        ];\n        $rules = self::callProtectedMethod($mock, 'processRuleFieldNames', [$rules]);\n\n        $this->assertEquals([\n            'field' => 'required',\n            'field.two' => 'required|boolean',\n            'field.three' => 'required|date',\n            'field.three.child' => 'required',\n            'field.four.*.name' => 'required',\n            'field[five' => 'required|string',\n            'field][six' => 'required|string',\n            'field]seven' => 'required|string',\n        ], $rules);\n    }\n}\n"
  },
  {
    "path": "tests/Html/HtmlBuilderTest.php",
    "content": "<?php\n\nuse October\\Rain\\Html\\HtmlBuilder;\n\nclass HtmlBuilderTest extends TestCase\n{\n    public function testStrip()\n    {\n        $result = with(new HtmlBuilder)->strip('<p>hello</p>');\n        $this->assertEquals('hello', $result);\n    }\n\n    public function testLimit()\n    {\n        $result = with(new HtmlBuilder)->limit('<p>The quick brown fox jumped over the lazy dog</p>', 10);\n        $this->assertEquals('<p>The quick ...</p>', $result);\n\n        $result = with(new HtmlBuilder)->limit(\"<p>The quick brown fox's jumped over the lazy dog</p>\", 25, '!!!');\n        $this->assertEquals(\"<p>The quick brown fox's jum!!!</p>\", $result);\n\n        $result = with(new HtmlBuilder)->limit(\"<p>The quick brown fox jumped over the lazy dog</p><p>The quick brown fox jumped over the lazy dog</p>\", 50);\n        $this->assertEquals('<p>The quick brown fox jumped over the lazy dog</p><p>The qu...</p>', $result);\n\n        $input = str_replace(\"\\r\\n\", \"\\n\", trim(\"\n            <p>The quick brown fox jumped over the lazy dog</p>\n            <p>The quick brown fox jumped over the lazy dog</p>\n        \"));\n        $result = with(new HtmlBuilder)->limit($input, 60);\n\n        $expected = str_replace(\"\\r\\n\", \"\\n\", trim('\n            <p>The quick brown fox jumped over the lazy dog</p>\n            <p>The...</p>\n        '));\n        $this->assertEquals($expected, $result);\n    }\n\n    //\n    // clean() tests - HTML sanitization\n    //\n\n    public function testCleanRemovesScriptTags()\n    {\n        $result = HtmlBuilder::clean('<script>window.location = \"http://google.com\"</script>');\n        $this->assertStringNotContainsString('<script', $result);\n        $this->assertStringNotContainsString('</script', $result);\n    }\n\n    public function testCleanRemovesStyleAttribute()\n    {\n        // Style attributes are stripped by the sanitizer for security\n        $result = HtmlBuilder::clean('<span style=\"width: expression(alert(\\'Ping!\\'));\"></span>');\n        $this->assertStringNotContainsString('expression', $result);\n    }\n\n    public function testCleanRemovesJavaScriptProtocol()\n    {\n        $result = HtmlBuilder::clean('<a href=\"javascript:alert(\\'Ping!\\');\">Test</a>');\n        $this->assertStringNotContainsString('javascript:', $result);\n\n        $result = HtmlBuilder::clean('<a href=\" &#14;  javascript: alert(\\'Ping!\\');\">Test</a>');\n        $this->assertStringNotContainsString('javascript:', $result);\n\n        $result = HtmlBuilder::clean('<a href=\" &#14  javascript: alert(\\'Ping!\\');\">Test</a>');\n        $this->assertStringNotContainsString('javascript:', $result);\n    }\n\n    public function testCleanRemovesVbScriptProtocol()\n    {\n        $result = HtmlBuilder::clean('<a href=\"vbscript:msgbox(\\'XSS\\')\">Test</a>');\n        $this->assertStringNotContainsString('vbscript:', $result);\n\n        // With spaces/encoding\n        $result = HtmlBuilder::clean('<a href=\"vb script:msgbox(\\'XSS\\')\">Test</a>');\n        $this->assertStringNotContainsString('vbscript', strtolower($result));\n    }\n\n    public function testCleanRemovesDataProtocol()\n    {\n        $result = HtmlBuilder::clean('<a href=\"data:text/html,<script>alert(1)</script>\">Test</a>');\n        $this->assertStringNotContainsString('data:', $result);\n\n        $result = HtmlBuilder::clean('<img src=\"data:image/svg+xml,<svg onload=alert(1)>\">');\n        $this->assertStringNotContainsString('data:', $result);\n    }\n\n    public function testCleanRemovesEventHandlers()\n    {\n        $result = HtmlBuilder::clean('<div onload=\"alert(1)\">content</div>');\n        $this->assertStringNotContainsString('onload', $result);\n\n        $result = HtmlBuilder::clean('<img src=\"x\" onerror=\"alert(1)\">');\n        $this->assertStringNotContainsString('onerror', $result);\n\n        $result = HtmlBuilder::clean('<body onmouseover=\"alert(1)\">');\n        $this->assertStringNotContainsString('onmouseover', $result);\n\n        $result = HtmlBuilder::clean('<div onclick=\"alert(1)\">click me</div>');\n        $this->assertStringNotContainsString('onclick', $result);\n\n        $result = HtmlBuilder::clean('<input onfocus=\"alert(1)\">');\n        $this->assertStringNotContainsString('onfocus', $result);\n    }\n\n    public function testCleanRemovesDangerousTags()\n    {\n        $result = HtmlBuilder::clean('<iframe src=\"evil.html\"></iframe>');\n        $this->assertStringNotContainsString('<iframe', $result);\n\n        $result = HtmlBuilder::clean('<object data=\"malicious.swf\"></object>');\n        $this->assertStringNotContainsString('<object', $result);\n\n        $result = HtmlBuilder::clean('<embed src=\"evil.swf\">');\n        $this->assertStringNotContainsString('<embed', $result);\n\n        $result = HtmlBuilder::clean('<applet code=\"malicious.class\"></applet>');\n        $this->assertStringNotContainsString('<applet', $result);\n\n        $result = HtmlBuilder::clean('<meta http-equiv=\"refresh\" content=\"0;url=evil.html\">');\n        $this->assertStringNotContainsString('<meta', $result);\n\n        $result = HtmlBuilder::clean('<link rel=\"stylesheet\" href=\"evil.css\">');\n        $this->assertStringNotContainsString('<link', $result);\n\n        $result = HtmlBuilder::clean('<base href=\"http://evil.com/\">');\n        $this->assertStringNotContainsString('<base', $result);\n\n        $result = HtmlBuilder::clean('<bgsound src=\"evil.mid\">');\n        $this->assertStringNotContainsString('<bgsound', $result);\n\n        $result = HtmlBuilder::clean('<frame src=\"evil.html\">');\n        $this->assertStringNotContainsString('<frame', $result);\n\n        $result = HtmlBuilder::clean('<frameset><frame src=\"evil.html\"></frameset>');\n        $this->assertStringNotContainsString('<frameset', $result);\n    }\n\n    public function testCleanRemovesStyleTags()\n    {\n        $result = HtmlBuilder::clean('<style>body { background: url(\"javascript:alert(1)\"); }</style>');\n        $this->assertStringNotContainsString('<style', $result);\n\n        $result = HtmlBuilder::clean('<style>@import \"evil.css\";</style>');\n        $this->assertStringNotContainsString('<style', $result);\n    }\n\n    public function testCleanRemovesXmlNamespacedTags()\n    {\n        $result = HtmlBuilder::clean('<xml:namespace prefix=\"o\" ns=\"urn:schemas-microsoft-com:office:office\">');\n        $this->assertStringNotContainsString('xml:', $result);\n\n        $result = HtmlBuilder::clean('<o:p>Office paragraph</o:p>');\n        $this->assertStringNotContainsString('<o:p', $result);\n    }\n\n    public function testCleanRemovesMozBinding()\n    {\n        $result = HtmlBuilder::clean('<div style=\"-moz-binding:url(\\'http://evil.com/xss.xml#xss\\')\">content</div>');\n        $this->assertStringNotContainsString('-moz-binding', $result);\n    }\n\n    public function testCleanPreservesValidHtml()\n    {\n        $result = HtmlBuilder::clean('<p>Hello <strong>world</strong></p>');\n        $this->assertStringContainsString('<p>', $result);\n        $this->assertStringContainsString('<strong>', $result);\n\n        $result = HtmlBuilder::clean('<a href=\"https://example.com\">Link</a>');\n        $this->assertStringContainsString('href=\"https://example.com\"', $result);\n\n        $result = HtmlBuilder::clean('<ul><li>Item 1</li><li>Item 2</li></ul>');\n        $this->assertStringContainsString('<ul>', $result);\n        $this->assertStringContainsString('<li>', $result);\n\n        $result = HtmlBuilder::clean('<blockquote>A quote</blockquote>');\n        $this->assertStringContainsString('<blockquote>', $result);\n\n        $result = HtmlBuilder::clean('<table><tr><td>Cell</td></tr></table>');\n        $this->assertStringContainsString('<table>', $result);\n        $this->assertStringContainsString('<td>', $result);\n    }\n\n    public function testCleanHandlesEntityEncodedAttacks()\n    {\n        // Hex encoded javascript\n        $result = HtmlBuilder::clean('<a href=\"&#x6A;&#x61;&#x76;&#x61;&#x73;&#x63;&#x72;&#x69;&#x70;&#x74;:alert(1)\">Test</a>');\n        $this->assertStringNotContainsString('javascript:', $result);\n\n        // Decimal encoded\n        $result = HtmlBuilder::clean('<a href=\"&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;:alert(1)\">Test</a>');\n        $this->assertStringNotContainsString('javascript:', $result);\n    }\n\n    public function testCleanCaseInsensitive()\n    {\n        $result = HtmlBuilder::clean('<SCRIPT>alert(1)</SCRIPT>');\n        $this->assertStringNotContainsString('<SCRIPT', $result);\n        $this->assertStringNotContainsString('<script', strtolower($result));\n\n        $result = HtmlBuilder::clean('<ScRiPt>alert(1)</ScRiPt>');\n        $this->assertStringNotContainsString('<ScRiPt', $result);\n\n        $result = HtmlBuilder::clean('<div ONLOAD=\"alert(1)\">test</div>');\n        $this->assertStringNotContainsString('ONLOAD', $result);\n        $this->assertStringNotContainsString('onload', strtolower($result));\n    }\n\n    public function testCleanHandlesNestedAttacks()\n    {\n        // Nested script tags\n        $result = HtmlBuilder::clean('<scr<script>ipt>alert(1)</scr</script>ipt>');\n        $this->assertStringNotContainsString('<script', strtolower($result));\n\n        // Double encoding\n        $result = HtmlBuilder::clean('<a href=\"java&amp;#115;cript:alert(1)\">Test</a>');\n        $this->assertStringNotContainsString('javascript:', $result);\n    }\n\n    //\n    // cleanVector() tests - SVG sanitization\n    //\n\n    public function testCleanVectorRemovesOnEventHandlers()\n    {\n        $result = HtmlBuilder::cleanVector('<svg onload=\"alert(1)\"></svg>');\n        $this->assertStringNotContainsString('onload', $result);\n\n        $result = HtmlBuilder::cleanVector('<svg onclick=\"alert(1)\"></svg>');\n        $this->assertStringNotContainsString('onclick', $result);\n\n        $result = HtmlBuilder::cleanVector('<svg onmouseover=\"alert(1)\"></svg>');\n        $this->assertStringNotContainsString('onmouseover', $result);\n\n        $result = HtmlBuilder::cleanVector('<svg onerror=\"alert(1)\"></svg>');\n        $this->assertStringNotContainsString('onerror', $result);\n\n        $result = HtmlBuilder::cleanVector('<svg onfocus=\"alert(1)\"></svg>');\n        $this->assertStringNotContainsString('onfocus', $result);\n    }\n\n    public function testCleanVectorBypassAttemptWithEmbeddedQuote()\n    {\n        // This is the specific bypass: a=\">\" tricks simple regex into thinking tag ends early\n        $result = HtmlBuilder::cleanVector('<svg xmlns=\"http://www.w3.org/2000/svg\" a=\">\" onload=\"alert(1)\"></svg>');\n        $this->assertStringNotContainsString('onload', $result);\n\n        // Variation with single quotes\n        $result = HtmlBuilder::cleanVector(\"<svg xmlns='http://www.w3.org/2000/svg' a='>' onload='alert(1)'></svg>\");\n        $this->assertStringNotContainsString('onload', $result);\n\n        // Multiple fake closures\n        $result = HtmlBuilder::cleanVector('<svg a=\">\" b=\">\" onload=\"alert(1)\"></svg>');\n        $this->assertStringNotContainsString('onload', $result);\n    }\n\n    public function testCleanVectorRemovesJavaScriptProtocol()\n    {\n        $result = HtmlBuilder::cleanVector('<svg><a href=\"javascript:alert(1)\">click</a></svg>');\n        $this->assertStringNotContainsString('javascript:', $result);\n\n        // With entity encoding\n        $result = HtmlBuilder::cleanVector('<svg><a href=\"&#106;avascript:alert(1)\">click</a></svg>');\n        $this->assertStringNotContainsString('javascript:', $result);\n    }\n\n    public function testCleanVectorRemovesVbScriptProtocol()\n    {\n        $result = HtmlBuilder::cleanVector('<svg><a href=\"vbscript:alert(1)\">click</a></svg>');\n        $this->assertStringNotContainsString('vbscript:', $result);\n    }\n\n    public function testCleanVectorRemovesDangerousTags()\n    {\n        // Script tag\n        $result = HtmlBuilder::cleanVector('<svg><script>alert(1)</script></svg>');\n        $this->assertStringNotContainsString('<script', $result);\n        $this->assertStringNotContainsString('</script', $result);\n\n        // Object tag\n        $result = HtmlBuilder::cleanVector('<svg><object data=\"malicious.swf\"></object></svg>');\n        $this->assertStringNotContainsString('<object', $result);\n\n        // Iframe tag\n        $result = HtmlBuilder::cleanVector('<svg><iframe src=\"evil.html\"></iframe></svg>');\n        $this->assertStringNotContainsString('<iframe', $result);\n\n        // Embed tag\n        $result = HtmlBuilder::cleanVector('<svg><embed src=\"evil.swf\"></embed></svg>');\n        $this->assertStringNotContainsString('<embed', $result);\n    }\n\n    public function testCleanVectorRemovesNamespacedElements()\n    {\n        $result = HtmlBuilder::cleanVector('<svg><foo:bar onload=\"alert(1)\">test</foo:bar></svg>');\n        $this->assertStringNotContainsString('foo:bar', $result);\n\n        $result = HtmlBuilder::cleanVector('<svg><xlink:href=\"javascript:alert(1)\"/></svg>');\n        $this->assertStringNotContainsString('xlink:', $result);\n    }\n\n    public function testCleanVectorPreservesValidSvgContent()\n    {\n        // Basic SVG structure should remain intact\n        $result = HtmlBuilder::cleanVector('<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"100\" height=\"100\"><rect x=\"10\" y=\"10\" width=\"80\" height=\"80\" fill=\"red\"/></svg>');\n        $this->assertStringContainsString('<svg', $result);\n        $this->assertStringContainsString('xmlns=', $result);\n        $this->assertStringContainsString('<rect', $result);\n        $this->assertStringContainsString('fill=\"red\"', $result);\n\n        // Style attribute should be preserved\n        $result = HtmlBuilder::cleanVector('<svg><rect style=\"fill:blue;\"/></svg>');\n        $this->assertStringContainsString('style=\"fill:blue;\"', $result);\n\n        // Title element should be preserved (allowed in cleanVector)\n        $result = HtmlBuilder::cleanVector('<svg><title>My SVG</title></svg>');\n        $this->assertStringContainsString('<title>My SVG</title>', $result);\n    }\n\n    public function testCleanVectorCaseInsensitive()\n    {\n        // Uppercase event handlers\n        $result = HtmlBuilder::cleanVector('<svg ONLOAD=\"alert(1)\"></svg>');\n        $this->assertStringNotContainsString('ONLOAD', $result);\n        $this->assertStringNotContainsString('onload', strtolower($result));\n\n        // Mixed case\n        $result = HtmlBuilder::cleanVector('<svg OnLoAd=\"alert(1)\"></svg>');\n        $this->assertStringNotContainsString('OnLoAd', $result);\n\n        // Uppercase tags\n        $result = HtmlBuilder::cleanVector('<svg><SCRIPT>alert(1)</SCRIPT></svg>');\n        $this->assertStringNotContainsString('<SCRIPT', $result);\n    }\n\n    public function testCleanVectorDataProtocol()\n    {\n        $result = HtmlBuilder::cleanVector('<svg><a href=\"data:text/html,<script>alert(1)</script>\">click</a></svg>');\n        $this->assertStringNotContainsString('data:', $result);\n    }\n}\n"
  },
  {
    "path": "tests/Html/HtmlHelperTest.php",
    "content": "<?php\n\nuse October\\Rain\\Html\\Helper as HtmlHelper;\n\nclass HtmlHelperTest extends TestCase\n{\n    public function testNameToId()\n    {\n        $result = HtmlHelper::nameToId('field');\n        $this->assertEquals('field', $result);\n\n        $result = HtmlHelper::nameToId('field[key1]');\n        $this->assertEquals('field-key1', $result);\n\n        $result = HtmlHelper::nameToId('field[][key1]');\n        $this->assertEquals('field--key1', $result);\n\n        $result = HtmlHelper::nameToId('field[key1][key2][key3]');\n        $this->assertEquals('field-key1-key2-key3', $result);\n    }\n\n    public function testNameToArray()\n    {\n        $result = HtmlHelper::nameToArray('field');\n        $this->assertIsArray($result);\n        $this->assertEquals(1, count($result));\n        $this->assertTrue(in_array('field', $result));\n\n        $result = HtmlHelper::nameToArray('field[key1]');\n        $this->assertIsArray($result);\n        $this->assertEquals(2, count($result));\n        $this->assertTrue(in_array('field', $result));\n        $this->assertTrue(in_array('key1', $result));\n\n        $result = HtmlHelper::nameToArray('field[][key1]');\n        $this->assertIsArray($result);\n        $this->assertEquals(2, count($result));\n        $this->assertTrue(in_array('field', $result));\n        $this->assertTrue(in_array('key1', $result));\n\n        $result = HtmlHelper::nameToArray('field[key1][key2][key3]');\n        $this->assertIsArray($result);\n        $this->assertEquals(4, count($result));\n        $this->assertTrue(in_array('field', $result));\n        $this->assertTrue(in_array('key1', $result));\n        $this->assertTrue(in_array('key2', $result));\n        $this->assertTrue(in_array('key3', $result));\n    }\n}\n"
  },
  {
    "path": "tests/Mail/MailerTest.php",
    "content": "<?php\n\nuse October\\Rain\\Mail\\Mailer;\nuse October\\Rain\\Mail\\FakeMailer;\nuse Symfony\\Component\\Mailer\\Transport\\NullTransport;\n\n/**\n * MailerTest\n */\nclass MailerTest extends TestCase\n{\n    /**\n     * testProcessRecipients\n     */\n    public function testProcessRecipients()\n    {\n        $mailer = $this->makeMailer();\n\n        /*\n         * String\n         */\n        $recipient = 'single@address.tld';\n        $result = self::callProtectedMethod($mailer, 'processRecipients', [$recipient]);\n        $this->assertCount(1, $result);\n        $this->assertArrayHasKey('single@address.tld', $result);\n        $this->assertNull($result['single@address.tld']);\n\n        /*\n         * Object\n         */\n        $recipients = (object) ['email' => 'user@domain.tld', 'name' => 'Adam Person'];\n        $result = self::callProtectedMethod($mailer, 'processRecipients', [$recipients]);\n        $this->assertCount(1, $result);\n        $this->assertArrayHasKey('user@domain.tld', $result);\n        $this->assertEquals('Adam Person', $result['user@domain.tld']);\n\n        /*\n         * Array\n         */\n        $recipients = [\n            'admin@domain.tld' => 'Adam Person',\n            'single@address.tld' => 'Pablo Francisco',\n            'charles@barrington.tld' => 'Charlie Sheen'\n        ];\n        $result = self::callProtectedMethod($mailer, 'processRecipients', [$recipients]);\n        $this->assertCount(3, $result);\n        $this->assertArrayHasKey('admin@domain.tld', $result);\n        $this->assertEquals('Adam Person', $result['admin@domain.tld']);\n        $this->assertArrayHasKey('single@address.tld', $result);\n        $this->assertEquals('Pablo Francisco', $result['single@address.tld']);\n        $this->assertArrayHasKey('charles@barrington.tld', $result);\n        $this->assertEquals('Charlie Sheen', $result['charles@barrington.tld']);\n\n        /*\n         * Array of Objects\n         */\n        $recipients = [\n            (object) ['email' => 'person@one.tld', 'name' => 'First Person'],\n            (object) ['email' => 'person@two.tld', 'name' => 'Second Person'],\n            (object) ['email' => 'person@three.tld', 'address' => 'Some address somewhere', 'name' => 'Third Person']\n        ];\n        $result = self::callProtectedMethod($mailer, 'processRecipients', [$recipients]);\n        $this->assertCount(3, $result);\n        $this->assertArrayHasKey('person@one.tld', $result);\n        $this->assertEquals('First Person', $result['person@one.tld']);\n        $this->assertArrayHasKey('person@two.tld', $result);\n        $this->assertEquals('Second Person', $result['person@two.tld']);\n        $this->assertArrayHasKey('person@three.tld', $result);\n        $this->assertEquals('Third Person', $result['person@three.tld']);\n\n        /*\n         * Array of Arrays\n         */\n        $recipients = [\n            ['email' => 'person@one.tld', 'name' => 'First Person'],\n            ['address' => 'person@two.tld', 'name' => 'Second Person'],\n            ['email' => 'person@three.tld', 'address' => 'XXX@two.tld', 'name' => 'Third Person']\n        ];\n        $result = self::callProtectedMethod($mailer, 'processRecipients', [$recipients]);\n        $this->assertCount(3, $result);\n        $this->assertArrayHasKey('person@one.tld', $result);\n        $this->assertEquals('First Person', $result['person@one.tld']);\n        $this->assertArrayHasKey('person@two.tld', $result);\n        $this->assertEquals('Second Person', $result['person@two.tld']);\n        $this->assertArrayHasKey('person@three.tld', $result);\n        $this->assertEquals('Third Person', $result['person@three.tld']);\n    }\n\n    //\n    // Helpers\n    //\n\n    protected static function callProtectedMethod($object, $name, $params = [])\n    {\n        $className = get_class($object);\n        $class = new ReflectionClass($className);\n        $method = $class->getMethod($name);\n        return $method->invokeArgs($object, $params);\n    }\n\n    //\n    // Mock\n    //\n\n    /**\n     * makeMailer\n     */\n    protected function makeMailer()\n    {\n        return new Mailer(\n            'test',\n            new FactoryMailerTest,\n            new NullTransport,\n            new DispatcherMailerTest\n        );\n    }\n}\n\nclass FactoryMailerTest extends \\Illuminate\\View\\Factory\n{\n    public function __construct()\n    {\n    }\n}\n\nclass DispatcherMailerTest extends \\Illuminate\\Events\\Dispatcher\n{\n    public function __construct()\n    {\n    }\n}\n"
  },
  {
    "path": "tests/Network/HttpTest.php",
    "content": "<?php\n\nuse October\\Rain\\Network\\Http;\nuse October\\Rain\\Exception\\ApplicationException;\n\nclass HttpTest extends TestCase\n{\n    const TEST_URL = 'http://somepath.tld';\n\n    public function testSetOptionsViaConstants()\n    {\n        $http = Http::make(self::TEST_URL, Http::METHOD_GET);\n        $http->setOption(CURLOPT_DNS_USE_GLOBAL_CACHE, true);\n        $http->setOption(CURLOPT_PIPEWAIT, false);\n        $http->setOption(CURLOPT_VERBOSE, true);\n\n        $this->assertEquals([\n            CURLOPT_DNS_USE_GLOBAL_CACHE => true,\n            CURLOPT_PIPEWAIT => false,\n            CURLOPT_VERBOSE => true\n        ], $http->requestOptions);\n\n        $http = Http::make(self::TEST_URL, Http::METHOD_GET);\n        $http->setOption('CURLOPT_DNS_USE_GLOBAL_CACHE', true);\n        $http->setOption('CURLOPT_PIPEWAIT', false);\n        $http->setOption('CURLOPT_VERBOSE', true);\n\n        $this->assertEquals([\n            CURLOPT_DNS_USE_GLOBAL_CACHE => true,\n            CURLOPT_PIPEWAIT => false,\n            CURLOPT_VERBOSE => true\n        ], $http->requestOptions);\n\n        $http = Http::make(self::TEST_URL, Http::METHOD_GET);\n        $http->setOption(91, true);   // CURLOPT_DNS_USE_GLOBAL_CACHE\n        $http->setOption(237, false); // CURLOPT_PIPEWAIT\n        $http->setOption(41, true);   // CURLOPT_VERBOSE\n\n        $this->assertEquals([\n            CURLOPT_DNS_USE_GLOBAL_CACHE => true,\n            CURLOPT_PIPEWAIT => false,\n            CURLOPT_VERBOSE => true\n        ], $http->requestOptions);\n    }\n\n    public function testSetInvalidOptionViaString()\n    {\n        $this->expectException(ApplicationException::class);\n        $this->expectExceptionMessage('$option parameter must be a CURLOPT constant or equivalent integer');\n\n        $http = Http::make(self::TEST_URL, Http::METHOD_GET);\n        $http->setOption('CURLOPT_SOME_RANDOM_CONSTANT', true);\n    }\n\n    public function testSetInvalidOptionViaInteger()\n    {\n        $this->expectException(ApplicationException::class);\n        $this->expectExceptionMessage('$option parameter must be a CURLOPT constant or equivalent integer');\n\n        $http = Http::make(self::TEST_URL, Http::METHOD_GET);\n        $http->setOption(99999, true);\n    }\n\n    public function testSetOptionsViaArrayOfConstants()\n    {\n        $http = Http::make(self::TEST_URL, Http::METHOD_GET);\n        $http->setOption([\n            CURLOPT_DNS_USE_GLOBAL_CACHE => true,\n            CURLOPT_PIPEWAIT => false,\n            CURLOPT_VERBOSE => true\n        ]);\n\n        $this->assertEquals([\n            CURLOPT_DNS_USE_GLOBAL_CACHE => true,\n            CURLOPT_PIPEWAIT => false,\n            CURLOPT_VERBOSE => true\n        ], $http->requestOptions);\n\n        $http = Http::make(self::TEST_URL, Http::METHOD_GET);\n        $http->setOption([\n            91 => true,   //CURLOPT_DNS_USE_GLOBAL_CACHE\n            237 => false, //CURLOPT_PIPEWAIT\n            41 => true    //CURLOPT_VERBOSE\n        ]);\n\n        $this->assertEquals([\n            CURLOPT_DNS_USE_GLOBAL_CACHE => true,\n            CURLOPT_PIPEWAIT => false,\n            CURLOPT_VERBOSE => true\n        ], $http->requestOptions);\n\n        $http = Http::make(self::TEST_URL, Http::METHOD_GET);\n        $http->setOption([\n            'CURLOPT_DNS_USE_GLOBAL_CACHE' => true,\n            'CURLOPT_PIPEWAIT' => false,\n            'CURLOPT_VERBOSE' => true\n        ]);\n\n        $this->assertEquals([\n            CURLOPT_DNS_USE_GLOBAL_CACHE => true,\n            CURLOPT_PIPEWAIT => false,\n            CURLOPT_VERBOSE => true\n        ], $http->requestOptions);\n    }\n\n    public function testSetInvalidOptionViaArrayOfStrings()\n    {\n        $this->expectException(ApplicationException::class);\n        $this->expectExceptionMessage('$option parameter must be a CURLOPT constant or equivalent integer');\n\n        $http = Http::make(self::TEST_URL, Http::METHOD_GET);\n        $http->setOption([\n            'CURLOPT_DNS_USE_GLOBAL_CACHE' => true,\n            'CURLOPT_PIPEWAIT' => false,\n            'CURLOPT_VERBOSE' => true,\n            'CURLOPT_SOME_RANDOM_CONSTANT' => true\n        ]);\n    }\n\n    public function testSetInvalidOptionViaArrayOfIntegers()\n    {\n        $this->expectException(ApplicationException::class);\n        $this->expectExceptionMessage('$option parameter must be a CURLOPT constant or equivalent integer');\n\n        $http = Http::make(self::TEST_URL, Http::METHOD_GET);\n        $http->setOption([\n            91 => true,   //CURLOPT_DNS_USE_GLOBAL_CACHE\n            237 => false, //CURLOPT_PIPEWAIT\n            41 => true,   //CURLOPT_VERBOSE\n            99999 => true // Invalid CURLOPT integer\n        ]);\n    }\n\n    public function testSetRequestDataGet()\n    {\n        // Scalar\n        $http = Http::make(self::TEST_URL, Http::METHOD_GET);\n        $http->data('foo', 'bar');\n        $this->assertEquals('foo=bar', $http->getRequestData());\n\n        // Array\n        $http = Http::make(self::TEST_URL, Http::METHOD_GET);\n        $http->data([\n            'foo' => 'bar',\n            'bar' => 'foo'\n        ]);\n        $this->assertEquals('foo=bar&bar=foo', $http->getRequestData());\n\n        // Option override (ignored)\n        $http->setOption(CURLOPT_POSTFIELDS, 'foobar');\n        $this->assertEquals('foo=bar&bar=foo', $http->getRequestData());\n\n        // MultiD Array\n        $http = Http::make(self::TEST_URL, Http::METHOD_GET);\n        $http->data([\n            'foo' => 'bar',\n            'bar' => 'foo',\n            'test' => ['a','b']\n        ]);\n        $this->assertEquals('foo=bar&bar=foo&test%5B0%5D=a&test%5B1%5D=b', $http->getRequestData());\n    }\n\n    public function testSetRequestDataPost()\n    {\n        // Scalar\n        $http = Http::make(self::TEST_URL, Http::METHOD_POST);\n        $http->data('foo', 'bar');\n        $this->assertEquals('foo=bar', $http->getRequestData());\n\n        // Array\n        $http = Http::make(self::TEST_URL, Http::METHOD_POST);\n        $http->data([\n            'foo' => 'bar',\n            'bar' => 'foo'\n        ]);\n        $this->assertEquals('foo=bar&bar=foo', $http->getRequestData());\n\n        // Option override (ignored)\n        $http->setOption(CURLOPT_POSTFIELDS, 'foobar');\n        $this->assertEquals('foo=bar&bar=foo', $http->getRequestData());\n\n        // Option override\n        $http = Http::make(self::TEST_URL, Http::METHOD_POST);\n        $http->setOption(CURLOPT_POSTFIELDS, 'foobar');\n        $this->assertEquals('foobar', $http->getRequestData());\n\n        // MultiD Array\n        $http = Http::make(self::TEST_URL, Http::METHOD_POST);\n        $http->data([\n            'foo' => 'bar',\n            'bar' => 'foo',\n            'test' => ['a','b']\n        ]);\n        $this->assertEquals('foo=bar&bar=foo&test%5B0%5D=a&test%5B1%5D=b', $http->getRequestData());\n    }\n\n    public function testSetRequestDataPostWithFiles()\n    {\n        // Array\n        $http = Http::make(self::TEST_URL, Http::METHOD_POST);\n        $http->data([\n            'foo' => 'bar',\n            'bar' => 'foo',\n            'test' => ['a','b']\n        ]);\n        $http->dataFile('testfile', __DIR__ . '/../fixtures/lang/en/lang.php');\n\n        $this->assertEquals([\n            'foo' => 'bar',\n            'bar' => 'foo',\n            'test' => '0=a&1=b',\n            'testfile' => new CURLFile(__DIR__ . '/../fixtures/lang/en/lang.php')\n        ], $http->getRequestData());\n    }\n}\n"
  },
  {
    "path": "tests/Parse/BracketTest.php",
    "content": "<?php\n\nuse October\\Rain\\Parse\\Bracket as TextParser;\n\nclass BracketTest extends TestCase\n{\n    public function testParseCombination()\n    {\n        $content = '{welcome}';\n        $content .= '{posts}{title}{/posts}';\n        $vars = [\n            'welcome' => 'Hello!',\n            'posts' => [\n                ['title' => 'Foo'],\n                ['title' => 'Bar'],\n            ]\n        ];\n        $result = TextParser::parse($content, $vars);\n        $this->assertEquals('Hello!FooBar', $result);\n    }\n\n    public function testParseSingleKey()\n    {\n        $content = '{foo} {foo} {foo}';\n        $vars = ['foo' => 'bar'];\n        $result = TextParser::parse($content, $vars);\n        $this->assertEquals('bar bar bar', $result);\n    }\n\n    public function testParseLoopingKey()\n    {\n        $content = '';\n        $content .= '{posts}{title}{/posts}';\n        // $content .= '{posts}{sound}{/posts}';\n        $vars = ['posts' => [\n            ['title' => 'Dog', 'sound' => 'Woof!'],\n            ['title' => 'Cat', 'sound' => 'Meow!'],\n        ]];\n        $result = TextParser::parse($content, $vars);\n        $this->assertEquals('DogCat', $result);\n    }\n\n    public function testParseWithFilters()\n    {\n        $filters = [];\n        $filters['upper'] = function ($value) {\n            return strtoupper($value);\n        };\n        $filters['lower'] = function ($value) {\n            return strtolower($value);\n        };\n\n        $content = '';\n        $content .= '{foo} {foo|upper} {foo|lower} ';\n        $content .= '{posts}{title}{title|upper}{title|lower}{/posts}';\n        $vars = [\n            'foo' => 'Bar',\n            'posts' => [\n                ['title' => 'Dog'],\n                ['title' => 'Cat'],\n            ]\n        ];\n        $result = TextParser::parse($content, $vars, ['filters' => $filters]);\n        $this->assertEquals('Bar BAR bar DogDOGdogCatCATcat', $result);\n    }\n}\n"
  },
  {
    "path": "tests/Parse/IniTest.php",
    "content": "<?php\n\nuse October\\Rain\\Parse\\Ini as IniParser;\n\nclass IniTest extends TestCase\n{\n\n    public function testBasic()\n    {\n        $path = __DIR__.'/../fixtures/parse/basic.ini';\n        $this->assertFileExists($path);\n        $content = $this->getContents($path);\n\n        $vars = [\n            'title' => 'Plugin components',\n            'url' => '/demo/plugins',\n            'layout' => 'default',\n            'demoTodo' => [\n                'min' => 1.2,\n                'max' => 3\n            ]\n        ];\n\n        $parser = new IniParser;\n        $result = $parser->parse($content);\n        $this->assertCount(4, $result);\n        $this->assertArrayHasKey('title', $result);\n        $this->assertArrayHasKey('url', $result);\n        $this->assertArrayHasKey('layout', $result);\n        $this->assertEquals('Plugin components', $result['title']);\n        $this->assertEquals('/demo/plugins', $result['url']);\n        $this->assertEquals('default', $result['layout']);\n        $this->assertArrayHasKey('demoTodo', $result);\n        $this->assertArrayHasKey('max', $result['demoTodo']);\n        $this->assertArrayHasKey('min', $result['demoTodo']);\n        $this->assertEquals(1.2, $result['demoTodo']['min']);\n        $this->assertEquals(3, $result['demoTodo']['max']);\n        $this->assertEquals($vars, $result);\n\n        $result = $parser->render($vars);\n        $this->assertEquals($content, $result);\n    }\n\n    public function testInvalid()\n    {\n        $this->expectException(Exception::class);\n        $this->expectExceptionMessage('Key name [ti=tle] is invalid for INI syntax');\n\n        $vars = [\n            'ti=tle' => 'Plugin components',\n        ];\n\n        $parser = new IniParser;\n        $parser->render($vars, ['exceptionOnInvalidKey' => true]);\n    }\n\n    public function testArray()\n    {\n        $path = __DIR__.'/../fixtures/parse/array.ini';\n        $this->assertFileExists($path);\n        $content = $this->getContents($path);\n\n        $vars = [\n            'products' => [\n                'excludeStatuses' => [1, 42, 69]\n            ]\n        ];\n\n        $parser = new IniParser;\n        $result = $parser->parse($content);\n        $this->assertArrayHasKey('products', $result);\n        $this->assertArrayHasKey('excludeStatuses', $result['products']);\n        $this->assertCount(3, $result['products']['excludeStatuses']);\n        $this->assertEquals($vars, $result);\n\n        $result = $parser->render($vars);\n        $this->assertEquals($content, $result);\n    }\n\n    public function testObject()\n    {\n        $path = __DIR__.'/../fixtures/parse/object.ini';\n        $this->assertFileExists($path);\n        $content = $this->getContents($path);\n\n        $vars = [\n            'viewBag' => [\n                'code' => 'signin-snippet',\n                'name' => 'Sign in snippet',\n                'properties' => [\n                    'type' => 'string',\n                    'title' => 'Redirection page',\n                    'default' => '/clients'\n                ]\n            ]\n        ];\n\n        $parser = new IniParser;\n        $result = $parser->parse($content);\n        $this->assertArrayHasKey('viewBag', $result);\n        $this->assertArrayHasKey('properties', $result['viewBag']);\n        $this->assertArrayHasKey('type', $result['viewBag']['properties']);\n        $this->assertArrayHasKey('title', $result['viewBag']['properties']);\n        $this->assertArrayHasKey('default', $result['viewBag']['properties']);\n        $this->assertEquals($vars, $result);\n\n        $result = $parser->render($vars);\n        $this->assertEquals($content, $result);\n    }\n\n    public function testComments()\n    {\n        $path = __DIR__.'/../fixtures/parse/comments.ini';\n        $this->assertFileExists($path);\n        $content = $this->getContents($path);\n\n        $vars = [\n            'owner' => [\n                'name' => 'John Doe',\n                'organization' => 'Acme Widgets Inc.',\n            ],\n            'database' => [\n                'server' => '192.0.2.62',\n                'port' => '143',\n                'file' => 'payroll.dat',\n            ]\n        ];\n\n        $parser = new IniParser;\n        $result = $parser->parse($content);\n        $this->assertArrayHasKey('owner', $result);\n        $this->assertArrayHasKey('name', $result['owner']);\n        $this->assertArrayHasKey('organization', $result['owner']);\n        $this->assertArrayHasKey('database', $result);\n        $this->assertArrayHasKey('server', $result['database']);\n        $this->assertArrayHasKey('port', $result['database']);\n        $this->assertArrayHasKey('file', $result['database']);\n        $this->assertEquals($vars, $result);\n\n        $path = __DIR__.'/../fixtures/parse/comments-clean.ini';\n        $this->assertFileExists($path);\n        $content = $this->getContents($path);\n\n        $content = preg_replace('~\\R~u', PHP_EOL, $content); // Normalize EOL\n\n        $result = $parser->render($vars);\n        $this->assertEquals($content, $result);\n    }\n\n    public function testComplex()\n    {\n        $path = __DIR__.'/../fixtures/parse/complex.ini';\n        $this->assertFileExists($path);\n        $content = $this->getContents($path);\n\n        $vars = [\n            'firstLevelValue' => 'relax',\n            'firstLevelArray' => ['foo', 'bar'],\n            'someComponent' => [\n                'secondLevelArray' => ['hello', 'world'],\n                'name' => [\n                    'title' => 'column_name_name',\n                    'validation' => [\n                        'required' => [\n                            'message' => 'column_name_required'\n                        ],\n                        'regex' => [\n                            'pattern' => '^[0-9_a-z]+$',\n                            'message' => 'column_validation_title'\n                        ]\n                    ]\n                ],\n                'type' => [\n                    'title' => 'column_name_type',\n                    'type' => 'dropdown',\n                    'options' => [\n                        'integer' => 'Integer',\n                        'smallInteger' => 'Small Integer',\n                        'bigInteger' => 'Big Integer',\n                        'date' => 'Date',\n                        'time' => 'Time',\n                        'dateTime' => 'Date and Time',\n                        'timestamp' => 'Timestamp',\n                        'string' => 'String',\n                        'text' => 'Text',\n                        'binary' => 'Binary',\n                        'boolean' => 'Boolean',\n                        'decimal' => 'Decimal',\n                        'double' => 'Double'\n                    ],\n                    'validation' => [\n                        'required' => [\n                            'message' => 'column_type_required'\n                        ]\n                    ]\n                ],\n                'modes' => [\n                    'title' => 'column_name_type',\n                    'type' => 'checkboxlist',\n                    'options' => [12, 34, 56, 78, 99]\n                ],\n                'security' => [\n                    'title' => 'column_name_security',\n                    'type' => 'radio',\n                    'options' => [\n                        'all' => ['All', 'Everyone'],\n                        'users' => ['Users', 'Users only'],\n                        'guests' => ['Guests', 'Guests only']\n                    ]\n                ],\n                'length' => [\n                    'title' => 'column_name_length',\n                    'validation' => [\n                        'regex' => [\n                            'pattern' => '(^[0-9]+$)|(^[0-9]+,[0-9]+$)',\n                            'message' => 'column_validation_length'\n                        ]\n                    ]\n                ],\n                'unsigned' => [\n                    'title' => 'column_name_unsigned',\n                    'type' => 'checkbox'\n                ],\n                'allow_null' => [\n                    'title' => 'column_name_nullable',\n                    'type' => 'checkbox'\n                ],\n                'auto_increment' => [\n                    'title' => 'column_auto_increment',\n                    'type' => 'checkbox'\n                ],\n                'primary_key' => [\n                    'title' => 'column_auto_primary_key',\n                    'type' => 'checkbox',\n                    'width' => '50px'\n                ],\n                'default' => [\n                    'title' => 'column_default'\n                ]\n            ]\n        ];\n\n        $parser = new IniParser;\n        $result = $parser->parse($content);\n        $this->assertEquals($vars, $result);\n\n        $result = $parser->render($vars);\n        $this->assertEquals($content, $result);\n    }\n\n    public function testMultilinesValues()\n    {\n        $path = __DIR__.'/../fixtures/parse/multilines-value.ini';\n        $this->assertFileExists($path);\n        $content = $this->getContents($path);\n\n        $vars = [\n            'var' => \"\\\\Test\\\\Path\\\\\",\n            'editorContent' =>\n                \"<p>Some\\n\" .\n                \"    <br>\\\"Multi-line\\\"\\n\" .\n                \"    <br>text\\n\" .\n                \"</p>\",\n        ];\n\n        $parser = new IniParser;\n        $result = $parser->parse($content);\n        $this->assertCount(2, $result);\n        $this->assertArrayHasKey('var', $result);\n        $this->assertArrayHasKey('editorContent', $result);\n\n        // Ensures we do not care about EOL sequences\n        $result['editorContent'] = str_replace(\"\\r\\n\", \"\\n\", $result['editorContent']);\n        $vars['editorContent'] = str_replace(\"\\r\\n\", \"\\n\", $vars['editorContent']);\n\n        $this->assertEquals($vars, $result);\n\n        $result = $parser->render($vars);\n        $content = str_replace(\"\\r\\n\", \"\\n\", $content);\n        $result = str_replace(\"\\r\\n\", \"\\n\", $result);\n        $this->assertEquals($content, $result);\n    }\n\n    public function testRender()\n    {\n        $parser = new IniParser;\n\n        $data = [\n            'var1'=>'value 1',\n            'var2'=>'value 21'\n        ];\n\n        $path = __DIR__.'/../fixtures/parse/simple.ini';\n        $this->assertFileExists($path);\n\n        $str = $parser->render($data);\n\n        $this->assertNotEmpty($str);\n        $this->assertEquals($this->getContents($path), $str);\n\n        $data = [\n            'section' => [\n                'sectionVar1' => 'section value 1',\n                'sectionVar2' => 'section value 2'\n            ],\n            'section data' => [\n                'sectionVar3' => 'section value 3',\n                'sectionVar4' => 'section value 4'\n            ],\n            'emptysection' => [],\n            'var1'=>'value 1',\n            'var2'=>'value 21'\n        ];\n\n        $path = __DIR__.'/../fixtures/parse/sections.ini';\n        $this->assertFileExists($path);\n\n        $str = $parser->render($data);\n        $this->assertEquals($this->getContents($path), $str);\n\n        $data = [\n            'section' => [\n                'sectionVar1' => 'section value 1',\n                'sectionVar2' => 'section value 2',\n                'subsection' => [\n                    'subsection value 1',\n                    'subsection value 2'\n                ],\n                'sectionVar3' => 'section value 3'\n            ],\n            'section data' => [\n                'sectionVar3' => 'section value 3',\n                'sectionVar4' => 'section value 4',\n                'subsection' => [\n                    'subsection value 1',\n                    'subsection value 2'\n                ]\n            ],\n            'var1'=>'value 1',\n            'var2'=>'value 21'\n        ];\n\n        $path = __DIR__.'/../fixtures/parse/subsections.ini';\n        $this->assertFileExists($path);\n\n        $str = $parser->render($data);\n        $this->assertEquals($this->getContents($path), $str);\n    }\n\n    public function testEnvironmentVariablesNotInterpolated()\n    {\n        putenv('OC_TEST_SECRET=should_not_leak');\n\n        $parser = new IniParser;\n\n        // Quoted value with ${VAR} syntax\n        $result = $parser->parse('key = \"${OC_TEST_SECRET}\"');\n        $this->assertEquals('${OC_TEST_SECRET}', $result['key']);\n\n        // Unquoted value with ${VAR} syntax\n        $result = $parser->parse('key = ${OC_TEST_SECRET}');\n        $this->assertEquals('${OC_TEST_SECRET}', $result['key']);\n\n        // Multiple env vars in a single value\n        $result = $parser->parse('key = \"${OC_TEST_SECRET} and ${OC_TEST_SECRET}\"');\n        $this->assertEquals('${OC_TEST_SECRET} and ${OC_TEST_SECRET}', $result['key']);\n\n        // Env var mixed with regular text\n        $result = $parser->parse('key = \"prefix_${OC_TEST_SECRET}_suffix\"');\n        $this->assertEquals('prefix_${OC_TEST_SECRET}_suffix', $result['key']);\n\n        // Env var in section values\n        $result = $parser->parse(\"[section]\\nkey = \\\"\\${OC_TEST_SECRET}\\\"\");\n        $this->assertArrayHasKey('section', $result);\n        $this->assertEquals('${OC_TEST_SECRET}', $result['section']['key']);\n\n        putenv('OC_TEST_SECRET');\n    }\n\n    public function testEnvironmentVariablesRoundTrip()\n    {\n        putenv('OC_TEST_SECRET=should_not_leak');\n\n        $parser = new IniParser;\n\n        $vars = [\n            'key' => '${OC_TEST_SECRET}',\n        ];\n\n        // Render and re-parse should preserve the ${VAR} syntax\n        $rendered = $parser->render($vars);\n        $result = $parser->parse($rendered);\n        $this->assertEquals($vars, $result);\n\n        putenv('OC_TEST_SECRET');\n    }\n\n    public function testEnvironmentVariablesCannotBeBypassed()\n    {\n        putenv('OC_TEST_SECRET=should_not_leak');\n\n        $parser = new IniParser;\n\n        // Double dollar should not leak\n        $result = $parser->parse('key = \"$${OC_TEST_SECRET}\"');\n        $this->assertStringNotContainsString('should_not_leak', $result['key']);\n\n        // Literal placeholder text in content should decode safely\n        $result = $parser->parse('key = \"oc_env_open{OC_TEST_SECRET}\"');\n        $this->assertEquals('${OC_TEST_SECRET}', $result['key']);\n        $this->assertStringNotContainsString('should_not_leak', $result['key']);\n\n        putenv('OC_TEST_SECRET');\n    }\n\n   //\n   // Helpers\n   //\n\n    protected function getContents($path)\n    {\n        $content = file_get_contents($path);\n        $content = preg_replace('~\\R~u', PHP_EOL, $content); // Normalize EOL\n        return $content;\n    }\n}\n"
  },
  {
    "path": "tests/Parse/MarkdownTest.php",
    "content": "<?php\n\nuse October\\Rain\\Parse\\Markdown;\nuse October\\Rain\\Events\\FakeDispatcher;\nuse October\\Rain\\Events\\Dispatcher;\n\n/**\n * MarkdownTest\n */\nclass MarkdownTest extends TestCase\n{\n    public function setUp(): void\n    {\n        parent::setUp();\n\n        Event::swap(new FakeDispatcher(new Dispatcher));\n    }\n\n    /**\n     * testParseIndent\n     */\n    public function testParseIndent()\n    {\n        $parser = new Markdown;\n\n        // Checking expectation\n        $text = <<<HTML\n            Code block\n        HTML;\n\n        $normal = $parser->parse($text);\n        $indent = $parser->parseIndent($text);\n\n        $this->assertEquals('<pre><code>Code block</code></pre>', $normal);\n        $this->assertEquals(\"<p>Code block</p>\", $indent);\n\n        // Checking a quirk\n        $text = \"##Hello world\\nSome other text\";\n\n        $normal = $parser->parse($text);\n        $indent = $parser->parseIndent($text);\n\n        $this->assertEquals(\"<h2>Hello world</h2>\\n<p>Some other text</p>\", $normal);\n        $this->assertEquals(\"<h2>Hello world</h2>\\n<p>Some other text</p>\", $indent);\n    }\n\n    /**\n     * testParseHtml\n     */\n    public function testParseHtml()\n    {\n        $parser = new Markdown;\n\n        // Check Markdown escaping\n        $text = <<<HTML\n<div>\n    This **text** won't be parsed by *Markdown*\n</div>\nHTML;\n\n        $normal = $parser->parse($text);\n\n        // Normalize values\n        $text = str_replace([\"\\r\", \"\\n\"], '', $text);\n        $normal = str_replace([\"\\r\", \"\\n\"], '', $normal);\n\n        $this->assertEquals(nl2br($text), nl2br($normal));\n\n        // Only accepting one node per line\n        $text = '<p>Foo</p><p>Bar</p>';\n        $normal = $parser->parse($text);\n        $this->assertEquals(\"<p>Foo</p>\", $normal);\n\n        // Wrapped as per docs\n        $text = '<div><p>Foo</p><p>Bar</p></div>';\n        $normal = $parser->parse($text);\n        $this->assertEquals(\"<div><p>Foo</p><p>Bar</p></div>\", $normal);\n    }\n\n    public function testParseNonHtml()\n    {\n        $parser = new Markdown;\n\n        $text = <<<TEXT\n<table\n\nsome other text\n\n## hello\n\nTEXT;\n\n$expected = '<p>&lt;table</p>\n<p>some other text</p>\n<h2>hello</h2>';\n\n        $normal = $parser->parse($text);\n\n        // Only accepting one node per line\n        $this->assertEquals(str_replace(\"\\r\\n\", \"\\n\", $expected), $normal);\n    }\n\n    public function testParseMultilineHtml()\n    {\n        $parser = new Markdown;\n\n        $text = <<<HTML\n<div>\n<table width=\"100%\"\n       align=\"center\"\n       border=\"0\"\n       cellpadding=\"0\"\n       cellspacing=\"0\"\n       style=\"background: red; min-height: 500px;\">\n    <thead>\n    <tr>\n        <th>Test</th>\n        <th>123</th>\n    </tr>\n    </thead>\n    <tbody>\n    <tr>\n        <td>Lorem</td>\n        <td>Ipsum</td>\n    </tr>\n    </tbody>\n</table>\n</div>\nHTML;\n\n        $expected = <<<HTML\n<div>\n<table width=\"100%\" align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"background: red; min-height: 500px;\">\n    <thead>\n    <tr>\n        <th>Test</th>\n        <th>123</th>\n    </tr>\n    </thead>\n    <tbody>\n    <tr>\n        <td>Lorem</td>\n        <td>Ipsum</td>\n    </tr>\n    </tbody>\n</table>\n</div>\nHTML;\n\n        $normal = $parser->parse($text);\n\n        $this->assertEquals(str_replace(\"\\r\\n\", \"\\n\", $expected), $normal);\n    }\n}\n"
  },
  {
    "path": "tests/Parse/SyntaxFieldParserTest.php",
    "content": "<?php\n\nuse October\\Rain\\Parse\\Syntax\\FieldParser;\n\nclass SyntaxFieldParserTest extends TestCase\n{\n\n    public function testParse()\n    {\n        $content = '';\n        $content .= '{text '.PHP_EOL.'name=\"field1\"'.PHP_EOL.' label=\"Field 1\"}{/text}'.PHP_EOL;\n        $content .= '{textarea name=\"field1\" label=\"Field 1 Again\"}{/textarea}'.PHP_EOL;\n        $content .= '{text name=\"field2\" label=\"Field 2\"}Default Text{/text}'.PHP_EOL;\n        $content .= '{textarea '.PHP_EOL.'name=\"field3\" '.PHP_EOL.'label=\"Field 3\"}Default Text{/textarea}'.PHP_EOL;\n        $content .= '{textarea name=\"field4\" label=\"Field 4\"}Invalid Tag{/invalid}'.PHP_EOL;\n\n        $result = FieldParser::parse($content);\n        $tags = $result->getTags();\n        $fields = $result->getFields();\n\n        $this->assertArrayHasKey('field1', $fields);\n        $this->assertArrayHasKey('field2', $fields);\n        $this->assertArrayHasKey('field3', $fields);\n        $this->assertArrayNotHasKey('field4', $fields);\n\n        $this->assertArrayHasKey('field1', $tags);\n        $this->assertArrayHasKey('field2', $tags);\n        $this->assertArrayHasKey('field3', $tags);\n        $this->assertArrayNotHasKey('field4', $tags);\n\n        $this->assertEquals('{textarea name=\"field1\" label=\"Field 1 Again\"}{/textarea}', $tags['field1']);\n\n        $this->assertArrayNotHasKey('name', $fields['field1']);\n        $this->assertArrayNotHasKey('name', $fields['field2']);\n        $this->assertArrayNotHasKey('name', $fields['field3']);\n\n        $this->assertArrayHasKey('type', $fields['field1']);\n        $this->assertArrayHasKey('type', $fields['field2']);\n        $this->assertArrayHasKey('type', $fields['field3']);\n\n        $this->assertEquals('textarea', $fields['field1']['type']);\n    }\n\n    public function testParseWithPrefix()\n    {\n        $content = '';\n        $content .= '{text '.PHP_EOL.'name=\"field1\"'.PHP_EOL.' label=\"Ignore me\"}{/oc:text}'.PHP_EOL;\n        $content .= '{text '.PHP_EOL.'name=\"field1\"'.PHP_EOL.' label=\"Field 1\"}{/text}'.PHP_EOL;\n        $content .= '{textarea name=\"field1\" label=\"Field 1 Again\"}{/textarea}'.PHP_EOL;\n        $content .= '{oc:text name=\"field2\" label=\"Field 2\"}Default Text{/oc:text}'.PHP_EOL;\n        $content .= '{oc:textarea '.PHP_EOL.'name=\"field3\" '.PHP_EOL.'label=\"Field 3\"}Default Text{/oc:textarea}'.PHP_EOL;\n        $content .= '{oc:textarea name=\"field4\" label=\"Field 4\"}Invalid Tag{/oc:invalid}'.PHP_EOL;\n        $content .= '{occ:textarea name=\"field1\" label=\"Ignore me too\"}{/occ:textarea}'.PHP_EOL;\n\n        $result = FieldParser::parse($content, ['tagPrefix' => 'oc:']);\n        $tags = $result->getTags();\n        $fields = $result->getFields();\n\n        $this->assertArrayHasKey('field1', $fields);\n        $this->assertArrayHasKey('field2', $fields);\n        $this->assertArrayHasKey('field3', $fields);\n        $this->assertArrayNotHasKey('field4', $fields);\n\n        $this->assertArrayHasKey('field1', $tags);\n        $this->assertArrayHasKey('field2', $tags);\n        $this->assertArrayHasKey('field3', $tags);\n        $this->assertArrayNotHasKey('field4', $tags);\n\n        $this->assertEquals('{textarea name=\"field1\" label=\"Field 1 Again\"}{/textarea}', $tags['field1']);\n        $this->assertEquals('{oc:text name=\"field2\" label=\"Field 2\"}Default Text{/oc:text}', $tags['field2']);\n\n        $this->assertArrayNotHasKey('name', $fields['field1']);\n        $this->assertArrayNotHasKey('name', $fields['field2']);\n        $this->assertArrayNotHasKey('name', $fields['field3']);\n\n        $this->assertArrayHasKey('type', $fields['field1']);\n        $this->assertArrayHasKey('type', $fields['field2']);\n        $this->assertArrayHasKey('type', $fields['field3']);\n\n        $this->assertEquals('textarea', $fields['field1']['type']);\n        $this->assertEquals('oc:text', $fields['field2']['type']);\n    }\n\n    public function testParseDropdownAndRadio()\n    {\n        $content = '';\n        $content .= '{dropdown name=\"field1\" label=\"Field 1\" options=\"one|two\"}{/dropdown}'.PHP_EOL;\n        $content .= '{radio name=\"field2\" label=\"Field 2\" options=\"y:Yes|n:No|m:Maybe\"}{/radio}'.PHP_EOL;\n        $content .= '{variable type=\"dropdown\" name=\"dropdown\" label=\"Pick one\" options=\"One|Two\"}{/variable}';\n        $content .= '{variable type=\"radio\" name=\"radio\" label=\"Thoughts?\" options=\"y:Yeah|n:Nah|m:Mebbe\"}{/variable}';\n\n        $result = FieldParser::parse($content);\n        $fields = $result->getFields();\n\n        $this->assertArrayHasKey('field1', $fields);\n        $this->assertArrayHasKey('field2', $fields);\n        $this->assertArrayHasKey('dropdown', $fields);\n        $this->assertArrayHasKey('radio', $fields);\n\n        $this->assertArrayHasKey('options', $fields['field1']);\n        $this->assertArrayHasKey('options', $fields['field2']);\n        $this->assertCount(2, $fields['field1']['options']);\n        $this->assertCount(3, $fields['field2']['options']);\n\n        $this->assertEquals(['one', 'two'], $fields['field1']['options']);\n        $this->assertEquals(['One', 'Two'], $fields['dropdown']['options']);\n\n        $this->assertEquals([\n            'y' => 'Yes',\n            'n' => 'No',\n            'm' => 'Maybe',\n        ], $fields['field2']['options']);\n\n        $this->assertEquals([\n            'y' => 'Yeah',\n            'n' => 'Nah',\n            'm' => 'Mebbe',\n        ], $fields['radio']['options']);\n    }\n\n    public function testParseRepeater()\n    {\n        $content = '';\n        $content .= '{text name=\"field1\" label=\"Field 1\"}{/text}'.PHP_EOL;\n        $content .= '{repeater name=\"repeater1\" label=\"Repeater 1\"}'.PHP_EOL;\n            $content .= '{textarea name=\"field1\" label=\"Field 1 Again\"}This Default Text{/textarea}'.PHP_EOL;\n            $content .= '{text name=\"field2\" label=\"Field 2\"}{/text}'.PHP_EOL;\n        $content .= '{/repeater}'.PHP_EOL;\n        $content .= '{textarea name=\"field3\" label=\"Field 3\"}Default Text{/textarea}'.PHP_EOL;\n        $content .= '{textarea name=\"field4\" label=\"Field 4\"}Invalid Tag{/invalid}'.PHP_EOL;\n        $content .= '{repeater name=\"repeater2\" label=\"Repeater 2\"}'.PHP_EOL;\n            $content .= '{textarea name=\"field5\" label=\"Field 5 Again\"}{/textarea}'.PHP_EOL;\n            $content .= '{text name=\"field6\" label=\"Field 6\"}Some Default Text{/text}'.PHP_EOL;\n        $content .= '{/repeater}'.PHP_EOL;\n        $content .= '{richeditor name=\"field7\" label=\"Field 7\"}{/richeditor}'.PHP_EOL;\n\n        $result = FieldParser::parse($content);\n        $tags = $result->getTags();\n        $fields = $result->getFields();\n\n        $this->assertArrayHasKey('field1', $fields);\n        $this->assertArrayNotHasKey('field2', $fields);\n        $this->assertArrayHasKey('field3', $fields);\n        $this->assertArrayHasKey('repeater1', $fields);\n        $this->assertArrayHasKey('repeater2', $fields);\n        $this->assertArrayNotHasKey('field5', $fields);\n        $this->assertArrayNotHasKey('field6', $fields);\n        $this->assertArrayHasKey('field7', $fields);\n        $this->assertArrayHasKey('fields', $fields['repeater1']);\n        $this->assertArrayHasKey('fields', $fields['repeater2']);\n        $this->assertArrayNotHasKey('default', $fields['repeater1']);\n        $this->assertArrayNotHasKey('default', $fields['repeater2']);\n\n        $this->assertArrayHasKey('field1', $tags);\n        $this->assertArrayNotHasKey('field2', $tags);\n        $this->assertArrayHasKey('field3', $tags);\n        $this->assertArrayHasKey('repeater1', $tags);\n        $this->assertArrayHasKey('repeater2', $tags);\n        $this->assertArrayNotHasKey('field5', $tags);\n        $this->assertArrayNotHasKey('field6', $tags);\n        $this->assertArrayHasKey('field7', $tags);\n\n        $this->assertArrayHasKey('tags', $tags['repeater1']);\n        $this->assertArrayHasKey('template', $tags['repeater1']);\n        $this->assertArrayHasKey('tags', $tags['repeater2']);\n        $this->assertArrayHasKey('template', $tags['repeater2']);\n\n        $this->assertArrayHasKey('field1', $tags['repeater1']['tags']);\n        $this->assertArrayHasKey('field2', $tags['repeater1']['tags']);\n        $this->assertArrayHasKey('field5', $tags['repeater2']['tags']);\n        $this->assertArrayHasKey('field6', $tags['repeater2']['tags']);\n\n        $this->assertEquals('{text name=\"field1\" label=\"Field 1\"}{/text}', $tags['field1']);\n        $this->assertEquals('{textarea name=\"field1\" label=\"Field 1 Again\"}This Default Text{/textarea}', $tags['repeater1']['tags']['field1']);\n        $this->assertEquals('{text name=\"field2\" label=\"Field 2\"}{/text}', $tags['repeater1']['tags']['field2']);\n        $this->assertEquals('{textarea name=\"field5\" label=\"Field 5 Again\"}{/textarea}', $tags['repeater2']['tags']['field5']);\n\n        $this->assertArrayNotHasKey('name', $fields['field1']);\n        $this->assertArrayNotHasKey('name', $fields['field3']);\n        $this->assertArrayNotHasKey('name', $fields['field7']);\n\n        $this->assertArrayHasKey('type', $fields['field1']);\n        $this->assertArrayHasKey('type', $fields['field3']);\n        $this->assertArrayHasKey('type', $fields['field7']);\n\n        $this->assertEquals('text', $fields['field1']['type']);\n        $this->assertEquals('repeater', $fields['repeater1']['type']);\n        $this->assertEquals('repeater', $fields['repeater2']['type']);\n        $this->assertEquals('textarea', $fields['repeater2']['fields']['field5']['type']);\n        $this->assertEquals('richeditor', $fields['field7']['type']);\n\n        $defaults = $result->getDefaultParams();\n        $this->assertEquals('Default Text', $defaults['field3']);\n        $this->assertEquals('This Default Text', $defaults['repeater1'][0]['field1']);\n        $this->assertEquals('Some Default Text', $defaults['repeater2'][0]['field6']);\n    }\n\n    public function testProcessTag()\n    {\n        $parser = new FieldParser;\n        $content = '';\n        $content .= '{text name=\"websiteName\" label=\"Website Name\" size=\"large\"}{/text}'.PHP_EOL;\n        $content .= '{text name=\"blogName\" label=\"Blog Name\" color=\"re\\\"d\"}October CMS{/text}'.PHP_EOL;\n        $content .= '{text name=\"storeName\" label=\"Store Name\" shape=\"circle\"}{/text}';\n        $content .= '{text label=\"Unnamed\" distance=\"400m\"}Foobar{/text}';\n        $content .= '{foobar name=\"nullName\" label=\"Valid tag, not searched by this test\"}{/foobar}';\n        list($tags, $fields) = self::callProtectedMethod($parser, 'processTags', [$content]);\n\n        $unnamedTag = md5('{text label=\"Unnamed\" distance=\"400m\"}Foobar{/text}');\n\n        $this->assertArrayNotHasKey('Unnamed', $fields);\n        $this->assertArrayNotHasKey('nullName', $fields);\n        $this->assertArrayHasKey('websiteName', $fields);\n        $this->assertArrayHasKey('blogName', $fields);\n        $this->assertArrayHasKey('storeName', $fields);\n        $this->assertArrayHasKey($unnamedTag, $fields);\n\n        $this->assertArrayNotHasKey('name', $fields['websiteName']);\n        $this->assertArrayHasKey('label', $fields['websiteName']);\n        $this->assertArrayHasKey('size', $fields['websiteName']);\n        $this->assertArrayHasKey('type', $fields['websiteName']);\n        $this->assertArrayHasKey('default', $fields['websiteName']);\n        $this->assertEquals('Website Name', $fields['websiteName']['label']);\n        $this->assertEquals('large', $fields['websiteName']['size']);\n        $this->assertEquals('text', $fields['websiteName']['type']);\n        $this->assertNotNull($fields['websiteName']['default']);\n        $this->assertEquals('', $fields['websiteName']['default']);\n\n        $this->assertArrayNotHasKey('name', $fields['blogName']);\n        $this->assertArrayHasKey('label', $fields['blogName']);\n        $this->assertArrayHasKey('color', $fields['blogName']);\n        $this->assertArrayHasKey('type', $fields['blogName']);\n        $this->assertArrayHasKey('default', $fields['blogName']);\n        $this->assertEquals('Blog Name', $fields['blogName']['label']);\n        $this->assertEquals('re\\\"d', $fields['blogName']['color']);\n        $this->assertEquals('text', $fields['blogName']['type']);\n        $this->assertNotNull($fields['blogName']['default']);\n        $this->assertEquals('October CMS', $fields['blogName']['default']);\n\n        $this->assertArrayNotHasKey('name', $fields['storeName']);\n        $this->assertArrayHasKey('label', $fields['storeName']);\n        $this->assertArrayHasKey('shape', $fields['storeName']);\n        $this->assertArrayHasKey('type', $fields['storeName']);\n        $this->assertArrayHasKey('default', $fields['storeName']);\n        $this->assertEquals('Store Name', $fields['storeName']['label']);\n        $this->assertEquals('circle', $fields['storeName']['shape']);\n        $this->assertEquals('text', $fields['storeName']['type']);\n        $this->assertNotNull($fields['storeName']['default']);\n        $this->assertEquals('', $fields['storeName']['default']);\n\n        $this->assertArrayNotHasKey('name', $fields[$unnamedTag]);\n        $this->assertArrayHasKey('label', $fields[$unnamedTag]);\n        $this->assertArrayHasKey('distance', $fields[$unnamedTag]);\n        $this->assertArrayHasKey('type', $fields[$unnamedTag]);\n        $this->assertArrayHasKey('default', $fields[$unnamedTag]);\n        $this->assertEquals('Unnamed', $fields[$unnamedTag]['label']);\n        $this->assertEquals('400m', $fields[$unnamedTag]['distance']);\n        $this->assertEquals('text', $fields[$unnamedTag]['type']);\n        $this->assertNotNull($fields[$unnamedTag]['default']);\n        $this->assertEquals('Foobar', $fields[$unnamedTag]['default']);\n\n        $this->assertArrayNotHasKey('Unnamed', $tags);\n        $this->assertArrayNotHasKey('nullName', $tags);\n        $this->assertArrayHasKey('websiteName', $tags);\n        $this->assertArrayHasKey('blogName', $tags);\n        $this->assertArrayHasKey('storeName', $tags);\n        $this->assertArrayHasKey($unnamedTag, $tags);\n    }\n\n    public function testProcessTagsRegex()\n    {\n        $parser = new FieldParser;\n        $content = '';\n        $content .= '{text name=\"websiteName\" label=\"Website Name\"}{/text}'.PHP_EOL;\n        $content .= '{text name=\"blogName\" label=\"Blog Name\"}October CMS{/text}'.PHP_EOL;\n        $content .= '{text name=\"storeName\" label=\"Store Name\"}{/text}';\n        $result = self::callProtectedMethod($parser, 'processTagsRegex', [$content, ['text']]);\n\n        $this->assertArrayHasKey(0, $result[2]);\n        $this->assertArrayHasKey(1, $result[2]);\n        $this->assertArrayHasKey(2, $result[2]);\n\n        $this->assertEquals('name=\"websiteName\" label=\"Website Name\"}', $result[2][0]);\n        $this->assertEquals('name=\"blogName\" label=\"Blog Name\"}October CMS', $result[2][1]);\n        $this->assertEquals('name=\"storeName\" label=\"Store Name\"}', $result[2][2]);\n    }\n\n    public function testProcessParamsRegex()\n    {\n        $parser = new FieldParser;\n        $content = 'name=\"test\" comment=\"This is a test\"';\n        $result = self::callProtectedMethod($parser, 'processParamsRegex', [$content]);\n\n        $this->assertArrayHasKey(0, $result[1]);\n        $this->assertArrayHasKey(1, $result[1]);\n        $this->assertArrayHasKey(0, $result[2]);\n        $this->assertArrayHasKey(1, $result[2]);\n        $this->assertEquals('name', $result[1][0]);\n        $this->assertEquals('comment', $result[1][1]);\n        $this->assertEquals('test', $result[2][0]);\n        $this->assertEquals('This is a test', $result[2][1]);\n\n        $content = 'name=\"te\\\"st\" comment=\"This\\\" is a test\"';\n        $result = self::callProtectedMethod($parser, 'processParamsRegex', [$content]);\n\n        $this->assertArrayHasKey(0, $result[1]);\n        $this->assertArrayHasKey(1, $result[1]);\n        $this->assertArrayHasKey(0, $result[2]);\n        $this->assertArrayHasKey(1, $result[2]);\n        $this->assertEquals('name', $result[1][0]);\n        $this->assertEquals('comment', $result[1][1]);\n        $this->assertEquals('te\\\"st', $result[2][0]);\n        $this->assertEquals('This\\\" is a test', $result[2][1]);\n    }\n\n    //\n    // Helpers\n    //\n\n    protected static function callProtectedMethod($object, $name, $params = [])\n    {\n        $className = get_class($object);\n        $class = new ReflectionClass($className);\n        $method = $class->getMethod($name);\n        return $method->invokeArgs($object, $params);\n    }\n\n    public static function getProtectedProperty($object, $name)\n    {\n        $className = get_class($object);\n        $class = new ReflectionClass($className);\n        $property = $class->getProperty($name);\n        return $property->getValue($object);\n    }\n\n    public static function setProtectedProperty($object, $name, $value)\n    {\n        $className = get_class($object);\n        $class = new ReflectionClass($className);\n        $property = $class->getProperty($name);\n        return $property->setValue($object, $value);\n    }\n}\n"
  },
  {
    "path": "tests/Parse/SyntaxParserTest.php",
    "content": "<?php\n\nuse October\\Rain\\Parse\\Syntax\\Parser;\n\nclass DropDownOptions\n{\n    public static function get()\n    {\n        return ['foo' => 'bar', 'bar' => 'foo'];\n    }\n}\n\nclass SyntaxParserTest extends TestCase\n{\n    public function testParseToTwig()\n    {\n        $content = '<h1>{text name=\"websiteName\" label=\"Website Name\"}Our wonderful website{/text}</h1>';\n\n        $result = Parser::parse($content)->toTwig();\n        $this->assertEquals('<h1>{{ websiteName }}</h1>', $result);\n\n        $result = Parser::parse($content, ['varPrefix' => 'joker.'])->toTwig();\n        $this->assertEquals('<h1>{{ joker.websiteName }}</h1>', $result);\n    }\n\n    public function testParseVariableToTwig()\n    {\n        $content = '<h1>{variable type=\"text\" name=\"websiteName\" label=\"Website Name\"}Our wonderful website{/variable}</h1>';\n\n        $result = Parser::parse($content)->toTwig();\n        $this->assertEquals('<h1></h1>', $result);\n    }\n\n    public function testParseRepeaterToTwig()\n    {\n        $content = '';\n        $content .= '{repeater name=\"websiteRepeater\" label=\"Website Repeater\"}'.PHP_EOL;\n        $content .= '<h1>{text name=\"websiteName\" label=\"Website Name\"}Our wonderful website{/text}</h1>'.PHP_EOL;\n        $content .= '{textarea name=\"websiteContent\" label=\"Website Content\"}Here are all the reasons we like our website{/textarea}'.PHP_EOL;\n        $content .= '{/repeater}'.PHP_EOL;\n\n        $result = Parser::parse($content)->toTwig();\n        $expected = '';\n        $expected .= '{% for fields in websiteRepeater %}'.PHP_EOL;\n        $expected .= '<h1>{{ fields.websiteName }}</h1>'.PHP_EOL;\n        $expected .= '{{ fields.websiteContent }}'.PHP_EOL;\n        $expected .= '{% endfor %}'.PHP_EOL;\n\n        $result = Parser::parse($content, ['varPrefix' => 'batman.'])->toTwig();\n        $expected = '';\n        $expected .= '{% for fields in batman.websiteRepeater %}'.PHP_EOL;\n        $expected .= '<h1>{{ fields.websiteName }}</h1>'.PHP_EOL;\n        $expected .= '{{ fields.websiteContent }}'.PHP_EOL;\n        $expected .= '{% endfor %}'.PHP_EOL;\n\n        $this->assertEquals($expected, $result);\n    }\n\n    public function testParseToView()\n    {\n        $content = '<h1>{text name=\"websiteName\" label=\"Website Name\"}Our wonderful website{/text}</h1>';\n\n        $result = Parser::parse($content)->toView();\n        $this->assertEquals('<h1>{websiteName}</h1>', $result);\n\n        $result = Parser::parse($content, ['varPrefix' => 'joker_'])->toView();\n        $this->assertEquals('<h1>{joker_websiteName}</h1>', $result);\n    }\n\n    public function testParseRepeaterToView()\n    {\n        $content = '';\n        $content .= '{repeater name=\"websiteRepeater\" label=\"Website Repeater\"}'.PHP_EOL;\n        $content .= '<h1>{text name=\"websiteName\" label=\"Website Name\"}Our wonderful website{/text}</h1>'.PHP_EOL;\n        $content .= '{textarea name=\"websiteContent\" label=\"Website Content\"}Here are all the reasons we like our website{/textarea}'.PHP_EOL;\n        $content .= '{/repeater}'.PHP_EOL;\n\n        $result = Parser::parse($content)->toView();\n        $expected = '';\n        $expected .= '{websiteRepeater}'.PHP_EOL;\n        $expected .= '<h1>{websiteName}</h1>'.PHP_EOL;\n        $expected .= '{websiteContent}'.PHP_EOL;\n        $expected .= '{/websiteRepeater}'.PHP_EOL;\n\n        $result = Parser::parse($content, ['varPrefix' => 'batman_'])->toView();\n        $expected = '';\n        $expected .= '{batman_websiteRepeater}'.PHP_EOL;\n        $expected .= '<h1>{websiteName}</h1>'.PHP_EOL;\n        $expected .= '{websiteContent}'.PHP_EOL;\n        $expected .= '{/batman_websiteRepeater}'.PHP_EOL;\n\n        $this->assertEquals($expected, $result);\n    }\n\n    public function testParseToEdit()\n    {\n        $content = '<h1>{text name=\"websiteName\" label=\"Website Name\"}Our wonderful website{/text}</h1>';\n\n        $result = Parser::parse($content)->toEditor();\n        $this->assertArrayHasKey('websiteName', $result);\n        $this->assertArrayHasKey('type', $result['websiteName']);\n        $this->assertArrayHasKey('default', $result['websiteName']);\n        $this->assertArrayHasKey('label', $result['websiteName']);\n        $this->assertEquals('text', $result['websiteName']['type']);\n        $this->assertEquals('Our wonderful website', $result['websiteName']['default']);\n        $this->assertEquals('Website Name', $result['websiteName']['label']);\n    }\n\n    public function testParseToRender()\n    {\n        $content = '<h1>{text name=\"websiteName\" label=\"Website Name\"}Our wonderful website{/text}</h1>';\n        $syntax = Parser::parse($content);\n\n        $result = $syntax->render();\n        $this->assertEquals('<h1>Our wonderful website</h1>', $result);\n\n        $result = $syntax->render(['websiteName' => 'Your awesome web page']);\n        $this->assertEquals('<h1>Your awesome web page</h1>', $result);\n    }\n\n    public function testParseRepeaterToRender()\n    {\n        $content = '';\n        $content .= '{repeater name=\"websiteRepeater\" label=\"Website Repeater\"}'.PHP_EOL;\n        $content .= '<h1>{text name=\"websiteName\" label=\"Website Name\"}Our wonderful website{/text}</h1>'.PHP_EOL;\n        $content .= '{textarea name=\"websiteContent\" label=\"Website Content\"}Here are all the reasons we like our website{/textarea}'.PHP_EOL;\n        $content .= '{/repeater}'.PHP_EOL;\n        $syntax = Parser::parse($content);\n\n        /*\n         * Default content\n         */\n        $result = $syntax->render();\n        $expected = '';\n        $expected .= PHP_EOL; // Repeater open\n        $expected .= '<h1>Our wonderful website</h1>'.PHP_EOL;\n        $expected .= 'Here are all the reasons we like our website'.PHP_EOL;\n        $expected .= PHP_EOL; // Repeater close\n        $this->assertEquals($expected, $result);\n\n        /*\n         * Multiple repeats\n         */\n        $sampleData = ['websiteRepeater' => [\n            [\n                'websiteName' => 'Moo',\n                'websiteContent' => 'Cow',\n            ],\n            [\n                'websiteName' => 'Foo',\n                'websiteContent' => 'Bar',\n            ]\n        ]];\n\n        $result = $syntax->render($sampleData);\n        $expected = '';\n        $expected .= PHP_EOL; // Repeater open\n        $expected .= '<h1>Moo</h1>'.PHP_EOL;\n        $expected .= 'Cow'.PHP_EOL;\n        $expected .= PHP_EOL; // Repeater divide\n        $expected .= '<h1>Foo</h1>'.PHP_EOL;\n        $expected .= 'Bar'.PHP_EOL;\n        $expected .= PHP_EOL; // Repeater close\n        $this->assertEquals($expected, $result);\n    }\n\n    public function testParseVariable()\n    {\n        $content = '{variable type=\"text\" name=\"websiteName\" label=\"Website Name\"}Our wonderful website{/variable}';\n\n        $result = Parser::parse($content)->toTwig();\n        $this->assertEquals('', $result);\n\n        $content = '{variable type=\"text\" name=\"websiteName\" label=\"Website Name\"}Our wonderful website{/variable}';\n\n        $result = Parser::parse($content)->toView();\n        $this->assertEquals('', $result);\n    }\n\n    public function testParseVariableToEdit()\n    {\n        $content = '{variable type=\"text\" name=\"websiteName\" label=\"Website Name\"}Our wonderful website{/variable}';\n\n        $result = Parser::parse($content)->toEditor();\n        $this->assertArrayHasKey('websiteName', $result);\n        $this->assertArrayHasKey('type', $result['websiteName']);\n        $this->assertArrayHasKey('default', $result['websiteName']);\n        $this->assertArrayHasKey('label', $result['websiteName']);\n        $this->assertEquals('text', $result['websiteName']['type']);\n        $this->assertEquals('Our wonderful website', $result['websiteName']['default']);\n        $this->assertEquals('Website Name', $result['websiteName']['label']);\n    }\n\n    public function testParseDropDownVariableToEdit()\n    {\n        $content = '{variable type=\"dropdown\" name=\"optionList\" label=\"Option List\" options=\"foo:bar|bar:foo\"}'\n            . '{/variable}';\n\n        $result = Parser::parse($content)->toEditor();\n\n        $this->assertArrayHasKey('optionList', $result);\n        $this->assertArrayHasKey('type', $result['optionList']);\n        $this->assertArrayHasKey('default', $result['optionList']);\n        $this->assertArrayHasKey('label', $result['optionList']);\n        $this->assertArrayHasKey('options', $result['optionList']);\n        $this->assertEquals('dropdown', $result['optionList']['type']);\n        $this->assertArrayHasKey('foo', $result['optionList']['options']);\n        $this->assertEquals('bar', $result['optionList']['options']['foo']);\n        $this->assertArrayHasKey('bar', $result['optionList']['options']);\n        $this->assertEquals('foo', $result['optionList']['options']['bar']);\n\n        $content = '{variable type=\"dropdown\" name=\"optionList\" label=\"Option List\" options=\"\\DropDownOptions::get\"}'\n            . '{/variable}';\n\n        $result = Parser::parse($content)->toEditor();\n\n        $this->assertArrayHasKey('optionList', $result);\n        $this->assertArrayHasKey('type', $result['optionList']);\n        $this->assertArrayHasKey('default', $result['optionList']);\n        $this->assertArrayHasKey('label', $result['optionList']);\n        $this->assertArrayHasKey('options', $result['optionList']);\n        $this->assertEquals('dropdown', $result['optionList']['type']);\n        $this->assertArrayHasKey('foo', $result['optionList']['options']);\n        $this->assertEquals('bar', $result['optionList']['options']['foo']);\n        $this->assertArrayHasKey('bar', $result['optionList']['options']);\n        $this->assertEquals('foo', $result['optionList']['options']['bar']);\n    }\n\n    public function testParseDropDownVariableToEditInvalidKeyException()\n    {\n        $this->expectException(Exception::class);\n\n        $content = '{variable type=\"dropdown\" name=\"optionList\" label=\"Option List\" options=\"^:one|*:two\"}'\n            . '{/variable}';\n\n        Parser::parse($content)->toEditor();\n    }\n\n    public function testParseDropDownVariableToEditInvalidStaticMethodException()\n    {\n        $this->expectException(Exception::class);\n\n        $content = '{variable type=\"dropdown\" name=\"optionList\" label=\"Option List\" options=\"\\Invalid\\Class\\Path::get\"}'\n            . '{/variable}';\n\n        Parser::parse($content)->toEditor();\n    }\n}\n"
  },
  {
    "path": "tests/Router/RouteTest.php",
    "content": "<?php\n\nuse October\\Rain\\Router\\Router;\n\nclass RouteTest extends TestCase\n{\n    public function testResolveUrl()\n    {\n        $params = [];\n        $router = new Router;\n\n        $rule = $router->reset()->route('testRuleId', 'blog/post');\n        $result = $rule->resolveUrl('/blog/post/10', $params);\n        $this->assertFalse($result);\n\n        $rule = $router->reset()->route('testRuleId', '/blog/post/:post_id');\n        $result = $rule->resolveUrl('blog/post/10', $params);\n        $this->assertTrue($result);\n        $this->assertEquals(1, count($params));\n        $this->assertArrayHasKey('post_id', $params);\n        $this->assertEquals(10, $params['post_id']);\n\n        $rule = $router->reset()->route('testRuleId', 'blog/post/:post_id?');\n        $result = $rule->resolveUrl('blog/post/', $params);\n        $this->assertTrue($result);\n        $this->assertEquals(1, count($params));\n        $this->assertArrayHasKey('post_id', $params);\n\n        $this->assertEquals(false, $params['post_id']);\n\n        $rule = $router->reset()->route('testRuleId', '/blog/post/:post_id?');\n        $result = $rule->resolveUrl('blog/post/my-post', $params);\n        $this->assertTrue($result);\n        $this->assertEquals(1, count($params));\n        $this->assertArrayHasKey('post_id', $params);\n        $this->assertEquals('my-post', $params['post_id']);\n\n        $rule = $router->reset()->route('testRuleId', '/blog/post/:post_id?|^[a-z\\-]+$');\n        $result = $rule->resolveUrl('blog/post/my-post', $params);\n        $this->assertTrue($result);\n        $this->assertEquals(1, count($params));\n        $this->assertArrayHasKey('post_id', $params);\n        $this->assertEquals('my-post', $params['post_id']);\n\n        $rule = $router->reset()->route('testRuleId', '/blog/post/:post_id|^[0-9]+$');\n        $result = $rule->resolveUrl('blog/post/10', $params);\n        $this->assertTrue($result);\n        $this->assertEquals(1, count($params));\n        $this->assertArrayHasKey('post_id', $params);\n        $this->assertEquals(10, $params['post_id']);\n\n        $rule = $router->reset()->route('testRuleId', '/blog/post/:post_id?|^[a-z\\-]+$');\n        $result = $rule->resolveUrl('blog/post/10', $params);\n        $this->assertFalse($result);\n\n        $rule = $router->reset()->route('testRuleId', '/authors/:author_id|^[a-z\\-]+$/details');\n        $result = $rule->resolveUrl('authors/my-author/details', $params);\n        $this->assertTrue($result);\n        $this->assertEquals(1, count($params));\n        $this->assertArrayHasKey('author_id', $params);\n        $this->assertEquals('my-author', $params['author_id']);\n\n        $rule = $router->reset()->route('testRuleId', '/authors/:author_id|^[a-z\\-]+$/details');\n        $result = $rule->resolveUrl('authors/details', $params);\n        $this->assertFalse($result);\n\n        $rule = $router->reset()->route('testRuleId', '/authors/:author_id?/details');\n        $result = $rule->resolveUrl('authors/details', $params);\n        $this->assertFalse($result);\n\n        $rule = $router->reset()->route('testRuleId', '/authors/:author_id?/details');\n        $result = $rule->resolveUrl('authors/test/details', $params);\n        $this->assertTrue($result);\n\n        $rule = $router->reset()->route('testRuleId', '/authors/:author_id?/:details?');\n        $result = $rule->resolveUrl('authors/test/details', $params);\n        $this->assertTrue($result);\n\n        $rule = $router->reset()->route('testRuleId', '/authors/:author_id?/:details?');\n        $result = $rule->resolveUrl('authors/test', $params);\n        $this->assertTrue($result);\n        $this->assertEquals(2, count($params));\n\n        $rule = $router->reset()->route('testRuleId', '/authors/:author_id|^[a-z\\-]+$/details/:record_type?|^[0-9]+$');\n        $result = $rule->resolveUrl('authors/my-author/details', $params);\n        $this->assertTrue($result);\n        $this->assertEquals(2, count($params));\n        $this->assertArrayHasKey('author_id', $params);\n        $this->assertEquals('my-author', $params['author_id']);\n        $this->assertArrayHasKey('record_type', $params);\n        $this->assertEquals(false, $params['record_type']);\n\n        $rule = $router->reset()->route('testRuleId', '/authors/:author_id|^[a-z\\-]+$/details/:record_type?|^[0-9]+$');\n        $result = $rule->resolveUrl('authors/my-author/details/441', $params);\n        $this->assertTrue($result);\n        $this->assertEquals(2, count($params));\n        $this->assertArrayHasKey('author_id', $params);\n        $this->assertEquals('my-author', $params['author_id']);\n        $this->assertArrayHasKey('record_type', $params);\n        $this->assertEquals('441', $params['record_type']);\n\n        $rule = $router->reset()->route('testRuleId', '/blog/post/:post_id|^[0-9]?$');\n        $result = $rule->resolveUrl('blog/post', $params);\n        $this->assertFalse($result);\n\n        $rule = $router->reset()->route('testRuleId', '/blog/post/:post_id/:post_slug|^my-slug-.*');\n        $result = $rule->resolveUrl('blog/post/4/no-slug-test', $params);\n        $this->assertFalse($result);\n\n        $rule = $router->reset()->route('testRuleId', '/blog/post/:post_id/:post_slug|^my-slug-.*');\n        $result = $rule->resolveUrl('blog/post/4/my-slug-test', $params);\n        $this->assertTrue($result);\n        $this->assertEquals(2, count($params));\n        $this->assertArrayHasKey('post_id', $params);\n        $this->assertArrayHasKey('post_slug', $params);\n        $this->assertEquals('4', $params['post_id']);\n        $this->assertEquals('my-slug-test', $params['post_slug']);\n\n        $rule = $router->reset()->route('testRuleId', '/blog/post/:post_id?my-post');\n        $result = $rule->resolveUrl('blog/post/', $params);\n        $this->assertTrue($result);\n        $this->assertEquals(1, count($params));\n        $this->assertArrayHasKey('post_id', $params);\n        $this->assertEquals('my-post', $params['post_id']);\n\n        $rule = $router->reset()->route('testRuleId', '/blog/post/:post_id?my-post|^[a-z]+$');\n        $result = $rule->resolveUrl('blog/post/', $params);\n        $this->assertTrue($result);\n        $this->assertEquals(1, count($params));\n        $this->assertArrayHasKey('post_id', $params);\n        $this->assertEquals('my-post', $params['post_id']);\n\n        $rule = $router->reset()->route('testRuleId', '/authors/:author_id?my-author-id|^[a-z\\-]+$/:record_type?15|^[0-9]+$');\n        $result = $rule->resolveUrl('authors', $params);\n        $this->assertTrue($result);\n        $this->assertEquals(2, count($params));\n        $this->assertArrayHasKey('author_id', $params);\n        $this->assertEquals('my-author-id', $params['author_id']);\n        $this->assertArrayHasKey('record_type', $params);\n        $this->assertEquals('15', $params['record_type']);\n\n        $rule = $router->reset()->route('testRuleId', '/color/:color/largecode/:largecode*/edit');\n        $result = $rule->resolveUrl('color/brown/largecode/code/with/slashes/edit', $params);\n        $this->assertTrue($result);\n        $this->assertEquals(2, count($params));\n        $this->assertArrayHasKey('color', $params);\n        $this->assertArrayHasKey('largecode', $params);\n        $this->assertEquals('brown', $params['color']);\n        $this->assertEquals('code/with/slashes', $params['largecode']);\n\n        $rule = $router->reset()->route('testRuleId', '/color/:color/largecode/:largecode*/edit');\n        $result = $rule->resolveUrl('color/brown/largecode/code/edit', $params);\n        $this->assertTrue($result);\n        $this->assertEquals(2, count($params));\n        $this->assertArrayHasKey('color', $params);\n        $this->assertArrayHasKey('largecode', $params);\n        $this->assertEquals('brown', $params['color']);\n        $this->assertEquals('code', $params['largecode']);\n\n        $rule = $router->reset()->route('testRuleId', '/color/:color/largecode/:largecode*/create');\n        $result = $rule->resolveUrl('color/brown/largecode/code/with/slashes/edit', $params);\n        $this->assertFalse($result);\n\n        $rule = $router->reset()->route('testRuleId', '/color/:color/largecode/:largecode*');\n        $result = $rule->resolveUrl('color/brown/largecode/code/with/slashes/edit', $params);\n        $this->assertTrue($result);\n        $this->assertEquals(2, count($params));\n        $this->assertArrayHasKey('color', $params);\n        $this->assertArrayHasKey('largecode', $params);\n        $this->assertEquals('brown', $params['color']);\n        $this->assertEquals('code/with/slashes/edit', $params['largecode']);\n\n        // Integer\n        $rule = $router->reset()->route('testRuleId', '/color/:color/largecode/:largecode*|^[a-z]+\\/[a-z]+$');\n        $result = $rule->resolveUrl('color/brown/largecode/code/100', $params);\n        $this->assertFalse($result);\n\n        // Too many segments\n        $rule = $router->reset()->route('testRuleId', '/color/:color/largecode/:largecode*|^[a-z]+\\/[a-z]+$');\n        $result = $rule->resolveUrl('color/brown/largecode/first/second/third', $params);\n        $this->assertFalse($result);\n\n        // Should pass @todo\n        // $rule = $router->reset()->route('testRuleId', '/color/:color/largecode/:largecode*|^[a-z]+\\/[a-z]+$');\n        // $result = $rule->resolveUrl('color/brown/largecode/first/second', $params);\n        // $this->assertTrue($result);\n\n        // $rule = $router->reset()->route('testRuleId', '/blog/:id*|^[0-9]+$');\n        // $this->assertFalse($rule->resolveUrl('blog/text/text', $params));\n        // $this->assertTrue($rule->resolveUrl('blog/2', $params));\n        // // Regex does not include a slash!\n        // $this->assertFalse($rule->resolveUrl('blog/2/3', $params));\n\n        // $rule = $router->reset()->route('testRuleId', '/blog/:page?*|^[0-9\\/]+$');\n        // $this->assertTrue($rule->resolveUrl('blog/2', $params));\n        // // Now it does\n        // $this->assertTrue($rule->resolveUrl('blog/2/3', $params));\n    }\n\n    /*\n     * Optimistic tests for Wildcard Regex support\n     */\n\n    // public function testWildcardRegexMatch()\n    // {\n    //     $router = new Router;\n\n    //     // Set up some dummy rules\n    //     $router->route('blogPagePost', '/blog/:page?*|^[0-9]+$');\n    //     $router->route('blogYearPost', '/blog/:year/:month/:slug*');\n\n    //     $router->match('/blog/2021/03/hello-world');\n    //     $this->assertEquals([\n    //         'year' => '2021',\n    //         'month' => '03',\n    //         'slug' => 'hello-world',\n    //     ], $router->getParameters());\n\n    //     $router->match('/blog/6');\n    //     $this->assertEquals(['page' => '6'], $router->getParameters());\n\n    //     $router = new Router;\n\n    //     // Set up some different dummy rules\n    //     $router->route('blogPagePost', '/blog/:page?*|^[0-9\\/]+$');\n    //     $router->route('blogYearCate', '/blog/category/:category*/:page?*|^[0-9\\/]+$');\n    //     $router->route('blogYearPost', '/blog/:year|^\\d{4}$/:month|^\\d{2}$/:day|^\\d{2}$/:slug');\n\n    //     $router->match('/blog/2');\n    //     $this->assertEquals(['page' => '2'], $router->getParameters());\n\n    //     $router->match('/blog/category/database');\n    //     $this->assertEquals(['category' => 'database', 'page' => false], $router->getParameters());\n\n    //     $router->match('/blog/category/database/page/2');\n    //     $this->assertEquals(['category' => 'database/page', 'page' => '2'], $router->getParameters());\n\n    //     $router->match('/blog/category/database/mysql');\n    //     $this->assertEquals(['category' => 'database/mysql'], $router->getParameters());\n\n    //     $router->match('/blog/category/database/mysql/page/2');\n    //     $this->assertEquals(['category' => 'database/mysql', 'page' => '2'], $router->getParameters());\n\n    //     $router->match('/blog/2021/03/31/importing-a-model-from-vroid-studio-into-unreal-engine');\n    //     $this->assertEquals([\n    //         'year' => '2021',\n    //         'month' => '03',\n    //         'day' => '31',\n    //         'slug' => 'importing-a-model-from-vroid-studio-into-unreal-engine'\n    //     ], $router->getParameters());\n    // }\n\n    public function testMatch()\n    {\n        $params = [];\n        $router = new Router;\n\n        // Set up some dummy rules\n        $router->route('authorDetails', '/authors/:author_id?/:details?');\n        $router->route('blogPost', 'blog/post');\n        $router->route('jobRequest', 'job/:type?request/:id');\n\n        $this->assertFalse($router->match('XXXXXXXXGARBAGE'));\n        $this->assertFalse($router->match('/XXXXXXXXGARBAGE'));\n\n        $this->assertFalse($router->match('/blog/post/10'));\n        $this->assertFalse($router->match('authors/test/details/more'));\n        $this->assertTrue($router->match('/authors'));\n        $this->assertTrue($router->match('authors/test/details'));\n\n        $this->assertFalse($router->match('job'));\n        $this->assertFalse($router->match('job/test'));\n        $this->assertTrue($router->match('job/test/4'));\n\n        // Wild routes come last\n        $router->route('testRuleWild', '/vehicles/:query?*');\n        $router->route('testRuleDynamic', '/vehicles/:make/:model/:id');\n        $router->sortRules();\n        $this->assertTrue($router->match('vehicles/audi/a3/123'));\n        $this->assertEquals('testRuleDynamic', $router->matchedRoute());\n    }\n\n    public function testSortRules()\n    {\n        $router = new Router;\n        $router->route('test1Static3Dynamic', '/vehicles/:make/:model/:id');\n        $router->route('test2Static3Dynamic', '/vehicles/long/:make/:model/:id');\n        $router->route('test3Static3Dynamic', '/vehicles/long/longer/:make/:model/:id');\n        $router->route('test3Static2Dynamic', '/vehicles/long/longer/:make/:model');\n        $router->route('test1Static1Wild', '/vehicles/:query?*');\n        $router->route('test2Static1Wild', '/vehicles/long/:query?*');\n        $router->route('test3Static1Wild', '/vehicles/long/longer/:query?*');\n        $router->route('test1Static', '/vehicles');\n        $router->route('test2Static', '/vehicles/long');\n        $router->route('test3Static', '/vehicles/long/longer');\n        $router->sortRules();\n\n        // static (long to short), dynamic (short to long), wild (at end)\n        $expecting = [\n            \"test3Static\",\n            \"test3Static2Dynamic\",\n            \"test3Static3Dynamic\",\n            \"test3Static1Wild\",\n            \"test2Static\",\n            \"test2Static3Dynamic\",\n            \"test2Static1Wild\",\n            \"test1Static\",\n            \"test1Static3Dynamic\",\n            \"test1Static1Wild\"\n        ];\n\n        $this->assertEquals($expecting, array_keys($router->getRouteMap()));\n    }\n\n    public function testUrl()\n    {\n        $params = [];\n        $router = new Router;\n\n        // Set up some dummy rules\n        $router->route('authorDetails', '/authors/:author_id?/:details?');\n        $router->route('blogPost', 'blog/post');\n        $router->route('userProfile', 'profile/:username');\n        $router->route('jobRequest', 'job/:type?request/:id');\n        $router->route('productPage', '/product/:category?/:id');\n        $router->route('portfolioPage', '/portfolio/:year?noYear/:category?noCategory/:budget?noBudget');\n\n        $result = $router->url('blogPost');\n        $this->assertEquals('/blog/post', $result);\n\n        $result = $router->url('authorDetails');\n        $this->assertEquals('/authors', $result);\n\n        $result = $router->url('authorDetails', ['author_id' => 20]);\n        $this->assertEquals('/authors/20', $result);\n\n        $result = $router->url('authorDetails', ['details' => 'history']);\n        $this->assertEquals('/authors/default/history', $result);\n\n        $result = $router->url('authorDetails', ['author_id' => 'default']);\n        $this->assertEquals('/authors/default', $result);\n\n        $result = $router->url('userProfile', ['username' => 'shaq']);\n        $this->assertEquals('/profile/shaq', $result);\n\n        $result = $router->url('jobRequest', ['id' => '9']);\n        $this->assertEquals('/job/request/9', $result);\n\n        $result = $router->url('jobRequest');\n        $this->assertEquals('/job/request/default', $result);\n\n        $result = $router->url('productPage', ['id' => '7']);\n        $this->assertEquals('/product/default/7', $result);\n\n        $result = $router->url('productPage', ['id' => '7', 'category' => 'helmets']);\n        $this->assertEquals('/product/helmets/7', $result);\n\n        $result = $router->url('portfolioPage');\n        $this->assertEquals('/portfolio', $result);\n\n        $result = $router->url('portfolioPage', ['year' => '2020']);\n        $this->assertEquals('/portfolio/2020', $result);\n\n        $result = $router->url('portfolioPage', ['category' => 'shoes']);\n        $this->assertEquals('/portfolio/noYear/shoes', $result);\n\n        $result = $router->url('portfolioPage', ['category' => null, 'budget' => '50000-above']);\n        $this->assertEquals('/portfolio/noYear/noCategory/50000-above', $result);\n\n        $result = $router->url('portfolioPage', ['year' => false, 'category' => null, 'budget' => 0]);\n        $this->assertEquals('/portfolio/noYear/noCategory/0', $result);\n\n        $result = $router->url('portfolioPage', ['budget' => 0]);\n        $this->assertEquals('/portfolio/noYear/noCategory/0', $result);\n\n        $result = $router->url('portfolioPage', ['year' => '2020', 'category' => 'noCategory']);\n        $this->assertEquals('/portfolio/2020', $result);\n\n        $result = $router->url('portfolioPage', ['year' => 'default', 'category' => 'noCategory', 'budget' => '200-500']);\n        $this->assertEquals('/portfolio/default/noCategory/200-500', $result);\n    }\n}\n"
  },
  {
    "path": "tests/Router/RouterHelperTest.php",
    "content": "<?php\n\nuse October\\Rain\\Router\\Helper;\n\n/**\n * RouterHelperTest\n */\nclass RouterHelperTest extends TestCase\n{\n    /**\n     * testvalidateUrl\n     */\n    public function testvalidateUrl()\n    {\n        $validUrls = [\n            '/october/cms',\n            '/october/:cms',\n            '/october/:cms?',\n            '/october/:cms?defaultvalue',\n            '/october/:cms|^[a-z]+[0-9]?$|^[a-z]{3}$',\n            '/october/:cms|^my-slug-.*',\n            '/october//cms',\n        ];\n\n        $invalidUrls = [\n            'october/cms',\n            '/october/cms#',\n            '/october/cms!',\n            '/october/cms?page=1',\n            '/october/c m s',\n            '/october/:cms?default value',\n        ];\n\n        foreach ($validUrls as $url) {\n            $this->assertTrue(Helper::validateUrl($url));\n        }\n\n        foreach ($invalidUrls as $url) {\n            $this->assertFalse(Helper::validateUrl($url));\n        }\n    }\n\n    /**\n     * testSegmentize\n     */\n    public function testSegmentize()\n    {\n        // Single param\n        $value = Helper::segmentizeUrl(\"/:my_param_name\");\n        $this->assertEquals([':my_param_name'], $value);\n\n        // Single param with regex\n        $value = Helper::segmentizeUrl('/:my_param_name|^[a-z]+[0-9]?$|^[a-z]{3}$');\n        $this->assertEquals([':my_param_name|^[a-z]+[0-9]?$|^[a-z]{3}$'], $value);\n\n        // Multiple params\n        $value = Helper::segmentizeUrl(\"/param1/:my_param_name\");\n        $this->assertEquals(['param1', ':my_param_name'], $value);\n\n        // Skip empty params\n        $value = Helper::segmentizeUrl(\"/param1//:my_param_name\");\n        $this->assertEquals(['param1', ':my_param_name'], $value);\n\n        // Multiple params with regex\n        $value = Helper::segmentizeUrl('/:my_param_name|^[a-z]+[0-9]?$|^[a-z]{3}$/param2');\n        $this->assertEquals([':my_param_name|^[a-z]+[0-9]?$|^[a-z]{3}$', 'param2'], $value);\n\n        // Escaped regex\n        $value = Helper::segmentizeUrl('/:my_param_name*|^[a-z]+\\/\\d+$');\n        $this->assertEquals([':my_param_name*|^[a-z]+\\/\\d+$'], $value);\n\n        // Escaped regex with multiple params\n        $value = Helper::segmentizeUrl('/:my_param_name*|^[a-z]+\\/\\d+$/param2');\n        $this->assertEquals([':my_param_name*|^[a-z]+\\/\\d+$', 'param2'], $value);\n    }\n\n    /**\n     * testSegmentIsWildcard\n     */\n    public function testSegmentIsWildcard()\n    {\n        $validSegments = [\n            ':my_param_name*',\n            ':my_param_name*|^[a-z]+[0-9]?$',\n        ];\n\n        $invalidSegments = [\n            ':my_param_name',\n            ':my_param_name|^[a-z]+[0-9]?$',\n            ':my_param_name|^[a-z]+[0-9]*',\n        ];\n\n        foreach ($validSegments as $name) {\n            $value = Helper::segmentIsWildcard($name);\n            $this->assertTrue($value, \"Testing segment name '{$name}' failed\");\n        }\n\n        foreach ($invalidSegments as $name) {\n            $value = Helper::segmentIsWildcard($name);\n            $this->assertFalse($value, \"Testing segment name '{$name}' failed\");\n        }\n    }\n\n    /**\n     * testSegmentIsOptional\n     */\n    public function testSegmentIsOptional()\n    {\n        $validSegments = [\n            ':my_param_name?',\n            ':my_param_name?default value',\n            ':my_param_name?default value|^[a-z]+[0-9]?$',\n        ];\n\n        $invalidSegments = [\n            ':my_param_name',\n            ':my_param_name|^[a-z]+[0-9]?$|^[a-z]{3}$',\n        ];\n\n        foreach ($validSegments as $name) {\n            $value = Helper::segmentIsOptional($name);\n            $this->assertTrue($value, \"Testing segment name '{$name}' failed\");\n        }\n\n        foreach ($invalidSegments as $name) {\n            $value = Helper::segmentIsOptional($name);\n            $this->assertFalse($value, \"Testing segment name '{$name}' failed\");\n        }\n    }\n\n    /**\n     * testParameterNameMethod\n     */\n    public function testParameterNameMethod()\n    {\n        $validParameters = [\n            ':my_param_name',\n            ':my_param_name*',\n            ':my_param_name?',\n            ':my_param_name*?',\n            ':my_param_name?default value',\n            ':my_param_name*?default value',\n            ':my_param_name|^[a-z]+[0-9]?$',\n            ':my_param_name*|^[a-z]+[0-9]?$',\n            ':my_param_name?default value|^[a-z]+[0-9]?$',\n            ':my_param_name|^my-slug-.*',\n            ':my_param_name?default value|^my-slug-.*',\n            ':my_param_name*|^my-slug-.*'\n        ];\n\n        foreach ($validParameters as $name) {\n            $value = Helper::getParameterName($name);\n            $this->assertEquals('my_param_name', $value, \"Testing parameter name '{$name}' failed\");\n        }\n    }\n\n    /**\n     * testSegmentRegexp\n     */\n    public function testSegmentRegexp()\n    {\n        $value = Helper::getSegmentRegExp(':my_param_name');\n        $this->assertFalse($value);\n\n        $value = Helper::getSegmentRegExp(':my_param_name?');\n        $this->assertFalse($value);\n\n        $value = Helper::getSegmentRegExp(':my_param_name?default value');\n        $this->assertFalse($value);\n\n        $value = Helper::getSegmentRegExp(':my_param_name|^[a-z]+[0-9]?$|^[a-z]{3}$');\n        $this->assertEquals('/^[a-z]+[0-9]?$|^[a-z]{3}$/', $value);\n\n        $value = Helper::getSegmentRegExp(':my_param_name?default value|^[a-z]+[0-9]?$');\n        $this->assertEquals('/^[a-z]+[0-9]?$/', $value);\n    }\n\n    /**\n     * testDefaultValue\n     */\n    public function testDefaultValue()\n    {\n        $value = Helper::getSegmentDefaultValue(':my_param_name');\n        $this->assertFalse($value);\n\n        $value = Helper::getSegmentDefaultValue(':my_param_name?');\n        $this->assertFalse($value);\n\n        $value = Helper::getSegmentDefaultValue(':my_param_name?default value');\n        $this->assertEquals('default value', $value);\n\n        $value = Helper::getSegmentDefaultValue(':my_param_name|^[a-z]+[0-9]?$|^[a-z]{3}$');\n        $this->assertFalse($value);\n\n        $value = Helper::getSegmentDefaultValue(':my_param_name?default value|^[a-z]+[0-9]?$');\n        $this->assertEquals('default value', $value);\n\n        // Wildcard with default value\n        $value = Helper::getSegmentDefaultValue(':my_param_name*?default value');\n        $this->assertEquals('default value', $value);\n\n        // Wildcard without default value\n        $value = Helper::getSegmentDefaultValue(':my_param_name*');\n        $this->assertFalse($value);\n\n        // Wildcard with empty default value\n        $value = Helper::getSegmentDefaultValue(':my_param_name*?');\n        $this->assertFalse($value);\n\n        // Wildcard after default value should not truncate\n        $value = Helper::getSegmentDefaultValue(':my_param_name?default*');\n        $this->assertEquals('default*', $value);\n\n        // Optional wildcard with no default (?* suffix)\n        $value = Helper::getSegmentDefaultValue(':query?*');\n        $this->assertFalse($value);\n\n        // Optional wildcard with regex and no default\n        $value = Helper::getSegmentDefaultValue(':page?*|^[0-9\\/]+$');\n        $this->assertFalse($value);\n    }\n\n    /**\n     * testReplaceParameters\n     */\n    public function testReplaceParameters()\n    {\n        $value = Helper::replaceParameters(['param1'=>'dynamic1'], 'static1/:param1/static2');\n        $this->assertEquals('static1/dynamic1/static2', $value);\n\n        $value = Helper::replaceParameters(['param1'=>'dynamic1'], 'static1/:param1/:param2');\n        $this->assertEquals('static1/dynamic1/:param2', $value);\n\n        $value = Helper::replaceParameters(['param1'=>'dynamic1', 'param2'=>'dynamic2'], 'static1/:param1/:param2');\n        $this->assertEquals('static1/dynamic1/dynamic2', $value);\n\n        $value = Helper::replaceParameters(['longer_param'=>'replacement'], 'Non-URL string: contains :longer_param, :other_param, and non-param colon');\n        $this->assertEquals('Non-URL string: contains replacement, :other_param, and non-param colon', $value);\n    }\n}\n"
  },
  {
    "path": "tests/Scaffold/ScaffoldBaseTest.php",
    "content": "<?php\n\nuse October\\Rain\\Scaffold\\GeneratorCommand;\n\nclass ScaffoldBaseTestCommand extends GeneratorCommand\n{\n    public function __construct()\n    {\n    }\n\n    protected function prepareVars()\n    {\n        return [];\n    }\n}\n\nclass ScaffoldBaseTest extends TestCase\n{\n    //\n    // Helpers\n    //\n\n    protected static function callProtectedMethod($object, $name, $params = [])\n    {\n        $className = get_class($object);\n        $class = new ReflectionClass($className);\n        $method = $class->getMethod($name);\n        return $method->invokeArgs($object, $params);\n    }\n\n    public static function getProtectedProperty($object, $name)\n    {\n        $className = get_class($object);\n        $class = new ReflectionClass($className);\n        $property = $class->getProperty($name);\n        return $property->getValue($object);\n    }\n\n    public static function setProtectedProperty($object, $name, $value)\n    {\n        $className = get_class($object);\n        $class = new ReflectionClass($className);\n        $property = $class->getProperty($name);\n        return $property->setValue($object, $value);\n    }\n\n    //\n    // Tests\n    //\n\n    public function testProcessVars()\n    {\n        $obj = new ScaffoldBaseTestCommand;\n\n        $testVars = [\n            'name' => 'duffMan',\n            'author' => 'Moe',\n            'plugin' => 'Duff',\n            'class' => 'TaxClass',\n        ];\n\n        $result = self::callProtectedMethod($obj, 'processVars', [$testVars]);\n\n        $this->assertArrayHasKey('name', $result);\n        $this->assertArrayHasKey('author', $result);\n        $this->assertArrayHasKey('plugin', $result);\n        $this->assertArrayHasKey('title_name', $result);\n        $this->assertArrayHasKey('title_author', $result);\n        $this->assertArrayHasKey('title_plugin', $result);\n        $this->assertArrayHasKey('upper_name', $result);\n        $this->assertArrayHasKey('upper_plural_name', $result);\n        $this->assertArrayHasKey('upper_singular_name', $result);\n        $this->assertArrayHasKey('lower_name', $result);\n        $this->assertArrayHasKey('lower_plural_name', $result);\n        $this->assertArrayHasKey('lower_singular_name', $result);\n        $this->assertArrayHasKey('snake_name', $result);\n        $this->assertArrayHasKey('snake_plural_name', $result);\n        $this->assertArrayHasKey('snake_singular_name', $result);\n        $this->assertArrayHasKey('studly_name', $result);\n        $this->assertArrayHasKey('studly_plural_name', $result);\n        $this->assertArrayHasKey('studly_singular_name', $result);\n        $this->assertArrayHasKey('camel_name', $result);\n        $this->assertArrayHasKey('camel_plural_name', $result);\n        $this->assertArrayHasKey('camel_singular_name', $result);\n        $this->assertArrayHasKey('plural_name', $result);\n        $this->assertArrayHasKey('singular_name', $result);\n        $this->assertArrayHasKey('upper_author', $result);\n        $this->assertArrayHasKey('upper_plural_author', $result);\n        $this->assertArrayHasKey('upper_singular_author', $result);\n        $this->assertArrayHasKey('lower_author', $result);\n        $this->assertArrayHasKey('lower_plural_author', $result);\n        $this->assertArrayHasKey('lower_singular_author', $result);\n        $this->assertArrayHasKey('snake_author', $result);\n        $this->assertArrayHasKey('snake_plural_author', $result);\n        $this->assertArrayHasKey('snake_singular_author', $result);\n        $this->assertArrayHasKey('studly_author', $result);\n        $this->assertArrayHasKey('studly_plural_author', $result);\n        $this->assertArrayHasKey('studly_singular_author', $result);\n        $this->assertArrayHasKey('camel_author', $result);\n        $this->assertArrayHasKey('camel_plural_author', $result);\n        $this->assertArrayHasKey('camel_singular_author', $result);\n        $this->assertArrayHasKey('plural_author', $result);\n        $this->assertArrayHasKey('singular_author', $result);\n        $this->assertArrayHasKey('upper_plugin', $result);\n        $this->assertArrayHasKey('upper_plural_plugin', $result);\n        $this->assertArrayHasKey('upper_singular_plugin', $result);\n        $this->assertArrayHasKey('lower_plugin', $result);\n        $this->assertArrayHasKey('lower_plural_plugin', $result);\n        $this->assertArrayHasKey('lower_singular_plugin', $result);\n        $this->assertArrayHasKey('snake_plugin', $result);\n        $this->assertArrayHasKey('snake_plural_plugin', $result);\n        $this->assertArrayHasKey('snake_singular_plugin', $result);\n        $this->assertArrayHasKey('studly_plugin', $result);\n        $this->assertArrayHasKey('studly_plural_plugin', $result);\n        $this->assertArrayHasKey('studly_singular_plugin', $result);\n        $this->assertArrayHasKey('camel_plugin', $result);\n        $this->assertArrayHasKey('camel_plural_plugin', $result);\n        $this->assertArrayHasKey('camel_singular_plugin', $result);\n        $this->assertArrayHasKey('plural_plugin', $result);\n        $this->assertArrayHasKey('singular_plugin', $result);\n\n        $this->assertEquals('duffMan', $result['name']);\n        $this->assertEquals('Moe', $result['author']);\n        $this->assertEquals('Duff', $result['plugin']);\n        $this->assertEquals('Duff Man', $result['title_name']);\n        $this->assertEquals('DUFFMAN', $result['upper_name']);\n        $this->assertEquals('DUFFMEN', $result['upper_plural_name']);\n        $this->assertEquals('DUFFMAN', $result['upper_singular_name']);\n        $this->assertEquals('duffman', $result['lower_name']);\n        $this->assertEquals('duffmen', $result['lower_plural_name']);\n        $this->assertEquals('duffman', $result['lower_singular_name']);\n        $this->assertEquals('duff_man', $result['snake_name']);\n        $this->assertEquals('duff_men', $result['snake_plural_name']);\n        $this->assertEquals('duff_man', $result['snake_singular_name']);\n        $this->assertEquals('DuffMan', $result['studly_name']);\n        $this->assertEquals('DuffMen', $result['studly_plural_name']);\n        $this->assertEquals('DuffMan', $result['studly_singular_name']);\n        $this->assertEquals('duffMan', $result['camel_name']);\n        $this->assertEquals('duffMen', $result['camel_plural_name']);\n        $this->assertEquals('duffMan', $result['camel_singular_name']);\n        $this->assertEquals('duffMen', $result['plural_name']);\n        $this->assertEquals('duffMan', $result['singular_name']);\n        $this->assertEquals('MOE', $result['upper_author']);\n        $this->assertEquals('MOES', $result['upper_plural_author']);\n        $this->assertEquals('MOE', $result['upper_singular_author']);\n        $this->assertEquals('moe', $result['lower_author']);\n        $this->assertEquals('moes', $result['lower_plural_author']);\n        $this->assertEquals('moe', $result['lower_singular_author']);\n        $this->assertEquals('moe', $result['snake_author']);\n        $this->assertEquals('moes', $result['snake_plural_author']);\n        $this->assertEquals('moe', $result['snake_singular_author']);\n        $this->assertEquals('Moe', $result['studly_author']);\n        $this->assertEquals('Moes', $result['studly_plural_author']);\n        $this->assertEquals('Moe', $result['studly_singular_author']);\n        $this->assertEquals('moe', $result['camel_author']);\n        $this->assertEquals('moes', $result['camel_plural_author']);\n        $this->assertEquals('moe', $result['camel_singular_author']);\n        $this->assertEquals('Moes', $result['plural_author']);\n        $this->assertEquals('Moe', $result['singular_author']);\n        $this->assertEquals('DUFF', $result['upper_plugin']);\n        $this->assertEquals('DUFFS', $result['upper_plural_plugin']);\n        $this->assertEquals('DUFF', $result['upper_singular_plugin']);\n        $this->assertEquals('duff', $result['lower_plugin']);\n        $this->assertEquals('duffs', $result['lower_plural_plugin']);\n        $this->assertEquals('duff', $result['lower_singular_plugin']);\n        $this->assertEquals('duff', $result['snake_plugin']);\n        $this->assertEquals('duffs', $result['snake_plural_plugin']);\n        $this->assertEquals('duff', $result['snake_singular_plugin']);\n        $this->assertEquals('Duff', $result['studly_plugin']);\n        $this->assertEquals('Duffs', $result['studly_plural_plugin']);\n        $this->assertEquals('Duff', $result['studly_singular_plugin']);\n        $this->assertEquals('duff', $result['camel_plugin']);\n        $this->assertEquals('duffs', $result['camel_plural_plugin']);\n        $this->assertEquals('duff', $result['camel_singular_plugin']);\n        $this->assertEquals('Duffs', $result['plural_plugin']);\n        $this->assertEquals('Duff', $result['singular_plugin']);\n        $this->assertEquals('Tax Class', $result['title_class']);\n        $this->assertEquals('Tax Class', $result['title_singular_class']);\n        $this->assertEquals('Tax Classes', $result['title_plural_class']);\n        $this->assertEquals('tax class', $result['lower_title_class']);\n    }\n}\n"
  },
  {
    "path": "tests/Support/CountableTest.php",
    "content": "<?php\nclass CountableTest extends TestCase\n{\n    public function testCountable()\n    {\n        $array = [\n            'foo' => 'bar',\n            'foo2' => 'bar2'\n        ];\n\n        $this->assertTrue(is_countable($array));\n\n        $collection = collect([\n            'foo' => 'bar',\n            'foo2' => 'bar2'\n        ]);\n\n        $this->assertTrue(is_countable($collection));\n\n        $arrayObj = new ArrayObject([\n            'foo' => 'bar',\n            'foo2' => 'bar2'\n        ]);\n\n        $this->assertTrue(is_countable($arrayObj));\n\n        $string = 'Test string';\n\n        $this->assertFalse(is_countable($string));\n\n        $int = 5;\n\n        $this->assertFalse(is_countable($int));\n\n        $emptyArray = [];\n\n        $this->assertTrue(is_countable($emptyArray));\n    }\n}\n"
  },
  {
    "path": "tests/Support/EmitterTest.php",
    "content": "<?php\n\nclass EmitterTest extends TestCase\n{\n    /**\n     * The object under test.\n     *\n     * @var object\n     */\n    private $traitObject;\n\n    /**\n     * Sets up the fixture.\n     *\n     * This method is called before a test is executed.\n     *\n     * @return void\n     */\n    public function setUp(): void\n    {\n        $traitName = 'October\\Rain\\Support\\Traits\\Emitter';\n        $this->traitObject = $this->getObjectForTrait($traitName);\n    }\n\n    //\n    // Tests\n    //\n\n    public function testBind()\n    {\n        $emitter = $this->traitObject;\n        $result = false;\n\n        $emitter->fireEvent('event.test');\n        $this->assertEquals(false, $result);\n\n        $emitter->bindEvent('event.test', function () use (&$result) {\n            $result = true;\n        });\n\n        $emitter->fireEvent('event.test');\n        $this->assertEquals(true, $result);\n    }\n\n    public function testBindOnce()\n    {\n        $emitter = $this->traitObject;\n        $result = 1;\n\n        $callback = function () use (&$result) {\n            $result++;\n        };\n\n        $emitter->bindEventOnce('event.test', $callback);\n        $emitter->fireEvent('event.test');\n        $emitter->fireEvent('event.test');\n        $emitter->fireEvent('event.test');\n\n        $this->assertEquals(2, $result);\n    }\n\n    public function testUnbindEvent()\n    {\n        $emitter = $this->traitObject;\n        $result = false;\n\n        $callback = function () use (&$result) {\n            $result = true;\n        };\n\n        $emitter->bindEvent('event.test', $callback);\n        $emitter->unbindEvent('event.test');\n        $emitter->fireEvent('event.test');\n\n        $this->assertEquals(false, $result);\n    }\n\n    public function testFireEvent()\n    {\n        $emitter = $this->traitObject;\n        $count = 0;\n\n        $callback = function () use (&$count) {\n            $count++;\n        };\n\n        $emitter->bindEvent('event.test', $callback);\n        $emitter->bindEvent('event.test', $callback);\n        $emitter->bindEvent('event.test', $callback);\n        $emitter->fireEvent('event.test');\n\n        $this->assertEquals(3, $count);\n    }\n\n    public function testFireEventResult()\n    {\n        $emitter = $this->traitObject;\n        $result = $emitter->fireEvent('event.test');\n        $this->assertEmpty($result);\n\n        $emitter->bindEvent('event.test', function () {\n            return 'foo';\n        });\n        $result = $emitter->fireEvent('event.test');\n        $this->assertNotNull($result);\n    }\n\n    public function testBindPriority()\n    {\n        $emitter = $this->traitObject;\n        $result = '';\n\n        // Skip code smell checks for this block of code.\n        // phpcs:disable\n        $emitter->bindEvent('event.test', function () use (&$result) { $result .= 'the '; }, 90);\n        $emitter->bindEvent('event.test', function () use (&$result) { $result .= 'quick '; }, 80);\n        $emitter->bindEvent('event.test', function () use (&$result) { $result .= 'brown '; }, 70);\n        $emitter->bindEvent('event.test', function () use (&$result) { $result .= 'fox '; }, 60);\n        $emitter->bindEvent('event.test', function () use (&$result) { $result .= 'jumped '; }, 50);\n        $emitter->bindEvent('event.test', function () use (&$result) { $result .= 'over '; }, 40);\n        $emitter->bindEvent('event.test', function () use (&$result) { $result .= 'the '; }, 30);\n        $emitter->bindEvent('event.test', function () use (&$result) { $result .= 'lazy '; }, 20);\n        $emitter->bindEvent('event.test', function () use (&$result) { $result .= 'dog'; }, 10);\n        $emitter->fireEvent('event.test');\n        // phpcs:enable\n\n        $this->assertEquals('the quick brown fox jumped over the lazy dog', $result);\n    }\n}\n"
  },
  {
    "path": "tests/Support/HttpBuildQueryTest.php",
    "content": "<?php\n\n/**\n * HttpBuildQueryTest\n */\nclass HttpBuildQueryTest extends TestCase\n{\n    /**\n     * testSimpleUrl\n     */\n    public function testSimpleUrl()\n    {\n        $this->assertEquals('https://octobercms.com/', http_build_url([\n            'scheme' => 'https',\n            'host' => 'octobercms.com',\n            'path' => '/'\n        ]));\n\n        $this->assertEquals('https://octobercms.com/', http_build_url([\n            'scheme' => 'https',\n            'host' => 'octobercms.com',\n            'path' => '/'\n        ]));\n    }\n\n    /**\n     * testComplexUrl\n     */\n    public function testComplexUrl()\n    {\n        $this->assertEquals('https://user:pass@github.com:80/octobercms/october?test=1&test=2#comment1', http_build_url([\n            'scheme' => 'https',\n            'user' => 'user',\n            'pass' => 'pass',\n            'host' => 'github.com',\n            'port' => 80,\n            'path' => '/octobercms/october',\n            'query' => 'test=1&test=2',\n            'fragment' => 'comment1'\n        ]));\n\n        $this->assertEquals('https://user:pass@github.com:80/octobercms/october?test=1&test=2#comment1', http_build_url([\n            'scheme' => 'https',\n            'user' => 'user',\n            'pass' => 'pass',\n            'host' => 'github.com',\n            'port' => 80,\n            'path' => '/octobercms/october',\n            'query' => 'test=1&test=2',\n            'fragment' => 'comment1'\n        ]));\n    }\n\n    /**\n     * testReplacements\n     */\n    public function testReplacements()\n    {\n        $this->assertEquals('https://octobercms.com', http_build_url([\n            'scheme' => 'https',\n            'host' => 'wordpress.org'\n        ], [\n            'scheme' => 'https',\n            'host' => 'octobercms.com'\n        ]));\n\n        $this->assertEquals('https://octobercms.com:80/changelog', http_build_url([\n            'scheme' => 'https',\n            'host' => 'octobercms.com'\n        ], [\n            'port' => 80,\n            'path' => '/changelog'\n        ]));\n\n        $this->assertEquals('ftp://username:password@ftp.test.com.au:21/newfolder', http_build_url([\n            'scheme' => 'https',\n            'user' => 'user',\n            'pass' => 'pass',\n            'host' => 'github.com',\n            'port' => 80,\n            'path' => '/octobercms/october',\n            'query' => 'test=1&test=2',\n            'fragment' => 'comment1'\n        ], [\n            'scheme' => 'ftp',\n            'user' => 'username',\n            'pass' => 'password',\n            'host' => 'ftp.test.com.au',\n            'port' => 21,\n            'path' => 'newfolder',\n            'query' => '',\n            'fragment' => ''\n        ]));\n    }\n\n    /**\n     * testJoinSegments\n     */\n    public function testJoinSegments()\n    {\n        $this->assertEquals('https://octobercms.com/plugins/rainlab-pages', http_build_url([\n            'scheme' => 'https',\n            'host' => 'octobercms.com',\n            'path' => '/plugins'\n        ], [\n            'path' => '/rainlab-pages'\n        ], HTTP_URL_JOIN_PATH));\n\n        $this->assertEquals('https://octobercms.com/?query1=1&query2=2&query3=3', http_build_url([\n            'scheme' => 'https',\n            'host' => 'octobercms.com',\n            'path' => '/',\n            'query' => 'query1=1&query2=2'\n        ], [\n            'query' => 'query3=3'\n        ], HTTP_URL_JOIN_QUERY));\n\n        $this->assertEquals('https://octobercms.com/plugins/rainlab-pages?query1=1&query2=2&query3=3', http_build_url([\n            'scheme' => 'https',\n            'host' => 'octobercms.com',\n            'path' => '/plugins',\n            'query' => 'query1=1&query2=2'\n        ], [\n            'path' => '/rainlab-pages',\n            'query' => 'query3=3'\n        ], HTTP_URL_JOIN_PATH | HTTP_URL_JOIN_QUERY));\n    }\n\n    /**\n     * testStripSegments\n     */\n    public function testStripSegments()\n    {\n        $segments = [\n            'scheme' => 'https',\n            'user' => 'user',\n            'pass' => 'pass',\n            'host' => 'github.com',\n            'port' => 80,\n            'path' => '/octobercms/october',\n            'query' => 'test=1&test=2',\n            'fragment' => 'comment1'\n        ];\n\n        $this->assertEquals(\n            'https://github.com:80/octobercms/october?test=1&test=2#comment1',\n            http_build_url($segments, [], HTTP_URL_STRIP_AUTH)\n        );\n\n        $this->assertEquals(\n            'https://github.com',\n            http_build_url($segments, [], HTTP_URL_STRIP_ALL)\n        );\n\n        $this->assertEquals(\n            'https://github.com:80/octobercms/october?test=1&test=2#comment1',\n            http_build_url($segments, [], HTTP_URL_STRIP_USER)\n        );\n\n        $this->assertEquals(\n            'https://user@github.com:80/octobercms/october?test=1&test=2#comment1',\n            http_build_url($segments, [], HTTP_URL_STRIP_PASS)\n        );\n\n        $this->assertEquals(\n            'https://user:pass@github.com/octobercms/october?test=1&test=2#comment1',\n            http_build_url($segments, [], HTTP_URL_STRIP_PORT)\n        );\n\n        $this->assertEquals(\n            'https://user:pass@github.com:80?test=1&test=2#comment1',\n            http_build_url($segments, [], HTTP_URL_STRIP_PATH)\n        );\n\n        $this->assertEquals(\n            'https://user:pass@github.com:80/octobercms/october#comment1',\n            http_build_url($segments, [], HTTP_URL_STRIP_QUERY)\n        );\n\n        $this->assertEquals(\n            'https://user:pass@github.com:80/octobercms/october?test=1&test=2',\n            http_build_url($segments, [], HTTP_URL_STRIP_FRAGMENT)\n        );\n\n        $this->assertEquals(\n            'https://user:pass@github.com/octobercms/october',\n            http_build_url($segments, [], HTTP_URL_STRIP_PORT | HTTP_URL_STRIP_QUERY | HTTP_URL_STRIP_FRAGMENT)\n        );\n    }\n}\n"
  },
  {
    "path": "tests/TestCase.php",
    "content": "<?php\n\nuse PHPUnit\\Framework\\Assert;\n\nclass TestCase extends PHPUnit\\Framework\\TestCase\n{\n    /**\n     * createApplication for the test\n     * @return \\Symfony\\Component\\HttpKernel\\HttpKernelInterface\n     */\n    public function createApplication()\n    {\n    }\n\n    /**\n     * callProtectedMethod\n     */\n    protected static function callProtectedMethod($object, $name, $params = [])\n    {\n        $className = get_class($object);\n        $class = new ReflectionClass($className);\n        $method = $class->getMethod($name);\n        return $method->invokeArgs($object, $params);\n    }\n\n    /**\n     * assertFileNotExists allows compatibility with PHPUnit 8 and 9\n     */\n    public static function assertFileNotExists(string $filename, string $message = ''): void\n    {\n        if (method_exists(Assert::class, 'assertFileDoesNotExist')) {\n            Assert::assertFileDoesNotExist($filename, $message);\n            return;\n        }\n\n        Assert::assertFileNotExists($filename, $message);\n    }\n\n    /**\n     * assertRegExp allows compatibility with PHPUnit 8 and 9\n     */\n    public static function assertRegExp(string $pattern, string $string, string $message = ''): void\n    {\n        if (method_exists(Assert::class, 'assertMatchesRegularExpression')) {\n            Assert::assertMatchesRegularExpression($pattern, $string, $message);\n            return;\n        }\n\n        Assert::assertRegExp($pattern, $string, $message);\n    }\n}\n"
  },
  {
    "path": "tests/Translation/TranslatorTest.php",
    "content": "<?php\n\nuse Illuminate\\Filesystem\\Filesystem;\nuse October\\Rain\\Translation\\FileLoader;\nuse October\\Rain\\Translation\\Translator;\n\nclass TranslatorTest extends TestCase\n{\n    /**\n     * @var Translator\n     */\n    protected $translator;\n\n    public function setUp(): void\n    {\n        parent::setUp();\n\n        $path = __DIR__ . '/../fixtures/lang';\n        $fileLoader = new FileLoader(new Filesystem(), $path);\n        $translator = new Translator($fileLoader, 'en');\n        $this->translator = $translator;\n    }\n\n    public function testSimilarWordsParsing()\n    {\n        $this->assertEquals(\n            'Displayed records: 1-100 of 10',\n            $this->translator->get('lang.test.pagination', ['from' => 1, 'to' => 100, 'total' => 10])\n        );\n    }\n\n    public function testOverrideWithSet()\n    {\n        $this->assertEquals('lang.test.hello_override', $this->translator->get('lang.test.hello_override'));\n        $this->translator->set('lang.test.hello_override', 'Hello Override!');\n        $this->assertEquals('Hello Override!', $this->translator->get('lang.test.hello_override'));\n    }\n}\n"
  },
  {
    "path": "tests/fixtures/config/sample-config.php",
    "content": "<?php\n\nreturn [\n\n    /*\n    |--------------------------------------------------------------------------\n    | Application Debug Mode\n    |--------------------------------------------------------------------------\n    |\n    | When your application is in debug mode, detailed error messages with\n    | stack traces will be shown on every error that occurs within your\n    | application. If disabled, a simple generic error page is shown.\n    |\n    */\n\n    'debug' => true,\n\n    \"debugAgain\"  =>  FALSE ,\n\n    \"bullyIan\" => 0,\n\n    'booLeeIan' => 1,\n\n    'aNumber' => 55,\n\n    'default' => 'mysql',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Application URL\n    |--------------------------------------------------------------------------\n    |\n    | This URL is used by the console to properly generate URLs when using\n    | the Artisan command line tool. You should set this to the root of\n    | your application so that it is used when running Artisan tasks.\n    |\n    */\n\n    'url' => 'http://localhost',\n\n    /*\n    |--------------------------------------------------------------------------\n    | Application Timezone\n    |--------------------------------------------------------------------------\n    |\n    | Here you may specify the default timezone for your application, which\n    | will be used by the PHP date and date-time functions. We have gone\n    | ahead and set this to a sensible default for you out of the box.\n    |\n    */\n\n    'timezone' => \"October's time\",\n\n    \"timezoneAgain\"               =>         'Something \"else\"'         ,\n\n    /*\n    |--------------------------------------------------------------------------\n    | Database Connections\n    |--------------------------------------------------------------------------\n    |\n    | Here are each of the database connections setup for your application.\n    | Of course, examples of configuring each database platform that is\n    | supported by Laravel is shown below to make development simple.\n    |\n    |\n    | All database work in Laravel is done through the PHP PDO facilities\n    | so make sure you have the driver for your particular database of\n    | choice installed on your machine before you begin development.\n    |\n    */\n\n    'connections' => [\n\n        'sqlite' => [\n            'driver'   => 'sqlite',\n            'database' => __DIR__.'/../database/production.sqlite',\n            'prefix'   => '',\n        ],\n\n        'mysql' => [\n            'driver'    => ['rabble' => 'mysql'],\n            'host'      => 'localhost',\n            'database'  => 'database',\n            'username'  => 'root',\n            'password'  => '',\n            'charset'   => 'utf8',\n            'collation' => 'utf8_unicode_ci',\n            'prefix'    => '',\n        ],\n\n        'sqlsrv' => [\n            'driver'   => 'sqlsrv',\n            'host'     => 'localhost',\n            'database' => 'database',\n            'username' => 'root',\n            'password' => '',\n            'prefix'   => '',\n        ],\n\n        'pgsql' => [\n            'driver'   => 'pgsql',\n            'host'     => 'localhost',\n            'database' => 'database',\n            'username' => 'root',\n            'password' => false,\n            'charset'  => 'utf8',\n            'prefix'   => '',\n            'schema'   => 'public',\n        ],\n    ],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Memcached Servers\n    |--------------------------------------------------------------------------\n    |\n    | Now you may specify an array of your Memcached servers that should be\n    | used when utilizing the Memcached cache driver. All of the servers\n    | should contain a value for \"host\", \"port\", and \"weight\" options.\n    |\n    */\n\n    'memcached' => ['host' => '127.0.0.1', 'port' => 11211, 'weight' => true],\n\n    /*\n    |--------------------------------------------------------------------------\n    | Redis Databases\n    |--------------------------------------------------------------------------\n    |\n    | Redis is an open source, fast, and advanced key-value store that also\n    | provides a richer set of commands than a typical key-value systems\n    | such as APC or Memcached. Laravel makes it easy to dig right in.\n    |\n    */\n\n    'redis' => [\n\n        'cluster' => false,\n\n        'default' => [\n            'host'     => '127.0.0.1',\n            'password' => null,\n            'port'     => 6379,\n            'database' => 0,\n        ],\n\n    ],\n];\n"
  },
  {
    "path": "tests/fixtures/database/SampleClass.php",
    "content": "<?php namespace TestPlugin;\n\nuse October\\Rain\\Supports\\Arr;\n\nclass SampleClass\n{\n    const SAMPLE = 'sample';\n\n    protected $sampleClass;\n\n    public function __construct()\n    {\n        if (class_exists(Arr::class)) {\n            $this->sampleClass = Arr::class;\n        }\n    }\n}\n"
  },
  {
    "path": "tests/fixtures/halcyon/models/Content.php",
    "content": "<?php\n\nuse October\\Rain\\Halcyon\\Model;\n\nclass HalcyonTestContent extends Model\n{\n    /**\n     * @var string The container name associated with the model, eg: pages.\n     */\n    protected $dirName = 'content';\n\n    /**\n     * @var array Allowable file extensions.\n     */\n    protected $allowedExtensions = ['htm', 'txt', 'md'];\n}\n"
  },
  {
    "path": "tests/fixtures/halcyon/models/Menu.php",
    "content": "<?php\n\nuse October\\Rain\\Halcyon\\Model;\n\nclass HalcyonTestMenu extends Model\n{\n\n    /**\n     * @var bool Model supports code and settings sections.\n     */\n    protected $isCompoundObject = false;\n\n    /**\n     * The attributes that are mass assignable.\n     *\n     * @var array\n     */\n    protected $fillable = [\n        'content',\n    ];\n\n    /**\n     * The container name associated with the model, eg: pages.\n     *\n     * @var string\n     */\n    protected $dirName = 'menus';\n}\n"
  },
  {
    "path": "tests/fixtures/halcyon/models/Page.php",
    "content": "<?php\n\nuse October\\Rain\\Halcyon\\Model;\n\nclass HalcyonTestPage extends Model\n{\n    /**\n     * @var int maxNesting\n     */\n    protected $maxNesting = 2;\n\n    /**\n     * The attributes that are mass assignable.\n     *\n     * @var array\n     */\n    protected $fillable = [\n        'title',\n        'viewBag',\n        'markup',\n        'code'\n    ];\n\n    /**\n     * The container name associated with the model, eg: pages.\n     *\n     * @var string\n     */\n    protected $dirName = 'pages';\n}\n\nclass HalcyonTestPageWithValidation extends HalcyonTestPage\n{\n    use \\October\\Rain\\Halcyon\\Traits\\Validation;\n\n    public $customMessages = [\n       'required' => 'The :attribute field is required.'\n    ];\n\n    public $attributeNames = [\n       'title' => 'title',\n       'viewBag.meta_title' => 'meta title'\n    ];\n\n    public $rules = [\n        'title' => 'required',\n        'viewBag.meta_title' => 'required'\n    ];\n}\n"
  },
  {
    "path": "tests/fixtures/halcyon/themes/theme1/content/welcome.htm",
    "content": "<p>Hi friend</p>"
  },
  {
    "path": "tests/fixtures/halcyon/themes/theme1/menus/mainmenu.htm",
    "content": "<ul><li>Home</li></ul>"
  },
  {
    "path": "tests/fixtures/halcyon/themes/theme1/pages/about.htm",
    "content": "title = \"About\"\n==\n<h1>Us</h1>\n"
  },
  {
    "path": "tests/fixtures/halcyon/themes/theme1/pages/home.htm",
    "content": "title = \"hello\"\n==\n<?\nfunction onStart() {}\n?>\n==\n<h1>World!</h1>\n"
  },
  {
    "path": "tests/fixtures/halcyon/themes/theme1/pages/level1/level2/level3/level4/level5/contact.htm",
    "content": "title = \"Contact\"\n==\n<h1>Us</h1>\n"
  },
  {
    "path": "tests/fixtures/halcyon/themes/theme1/pages/level1/level2/level3/level4/level5/level6/unknown.htm",
    "content": "title = \"Unknown\"\n==\n<h1>Page</h1>\n"
  },
  {
    "path": "tests/fixtures/halcyon/themes/theme1/pages/level1/team.htm",
    "content": "title = \"Team\"\n==\n<h1>Join Us</h1>\n"
  },
  {
    "path": "tests/fixtures/halcyon/themes/theme2/pages/home.htm",
    "content": "title = \"Cold\"\n==\n<h1>Chisel</h1>"
  },
  {
    "path": "tests/fixtures/lang/en/lang.php",
    "content": "<?php\n\nreturn [\n    'test' => [\n        'pagination' => 'Displayed records: :from-:to of :total',\n        'hello_october' => 'Hello October!'\n    ],\n];\n"
  },
  {
    "path": "tests/fixtures/parse/array.ini",
    "content": "[products]\nexcludeStatuses[] = 1\nexcludeStatuses[] = 42\nexcludeStatuses[] = 69"
  },
  {
    "path": "tests/fixtures/parse/basic.ini",
    "content": "title = \"Plugin components\"\nurl = \"/demo/plugins\"\nlayout = \"default\"\n\n[demoTodo]\nmin = 1.2\nmax = 3"
  },
  {
    "path": "tests/fixtures/parse/comments-clean.ini",
    "content": "[owner]\nname = \"John Doe\"\norganization = \"Acme Widgets Inc.\"\n\n[database]\nserver = \"192.0.2.62\"\nport = 143\nfile = \"payroll.dat\""
  },
  {
    "path": "tests/fixtures/parse/comments.ini",
    "content": "; last modified 1 April 2001 by John Doe\n[owner]\nname=John Doe\n; name=Adam Person\norganization=Acme Widgets Inc.\n\n[database]\n; use IP address in case network name resolution is not working\nserver=192.0.2.62\n; server=127.0.0.1\nport=143\nfile=\"payroll.dat\""
  },
  {
    "path": "tests/fixtures/parse/complex.ini",
    "content": "firstLevelValue = \"relax\"\nfirstLevelArray[] = \"foo\"\nfirstLevelArray[] = \"bar\"\n\n[someComponent]\nsecondLevelArray[] = \"hello\"\nsecondLevelArray[] = \"world\"\nname[title] = \"column_name_name\"\nname[validation][required][message] = \"column_name_required\"\nname[validation][regex][pattern] = \"^[0-9_a-z]+$\"\nname[validation][regex][message] = \"column_validation_title\"\ntype[title] = \"column_name_type\"\ntype[type] = \"dropdown\"\ntype[options][integer] = \"Integer\"\ntype[options][smallInteger] = \"Small Integer\"\ntype[options][bigInteger] = \"Big Integer\"\ntype[options][date] = \"Date\"\ntype[options][time] = \"Time\"\ntype[options][dateTime] = \"Date and Time\"\ntype[options][timestamp] = \"Timestamp\"\ntype[options][string] = \"String\"\ntype[options][text] = \"Text\"\ntype[options][binary] = \"Binary\"\ntype[options][boolean] = \"Boolean\"\ntype[options][decimal] = \"Decimal\"\ntype[options][double] = \"Double\"\ntype[validation][required][message] = \"column_type_required\"\nmodes[title] = \"column_name_type\"\nmodes[type] = \"checkboxlist\"\nmodes[options][] = 12\nmodes[options][] = 34\nmodes[options][] = 56\nmodes[options][] = 78\nmodes[options][] = 99\nsecurity[title] = \"column_name_security\"\nsecurity[type] = \"radio\"\nsecurity[options][all][] = \"All\"\nsecurity[options][all][] = \"Everyone\"\nsecurity[options][users][] = \"Users\"\nsecurity[options][users][] = \"Users only\"\nsecurity[options][guests][] = \"Guests\"\nsecurity[options][guests][] = \"Guests only\"\nlength[title] = \"column_name_length\"\nlength[validation][regex][pattern] = \"(^[0-9]+$)|(^[0-9]+,[0-9]+$)\"\nlength[validation][regex][message] = \"column_validation_length\"\nunsigned[title] = \"column_name_unsigned\"\nunsigned[type] = \"checkbox\"\nallow_null[title] = \"column_name_nullable\"\nallow_null[type] = \"checkbox\"\nauto_increment[title] = \"column_auto_increment\"\nauto_increment[type] = \"checkbox\"\nprimary_key[title] = \"column_auto_primary_key\"\nprimary_key[type] = \"checkbox\"\nprimary_key[width] = \"50px\"\ndefault[title] = \"column_default\""
  },
  {
    "path": "tests/fixtures/parse/multilines-value.ini",
    "content": "var = \"\\Test\\Path\\\"\neditorContent = \"<p>Some\n    <br>\\\"Multi-line\\\"\"\"\n    <br>text\n</p>\""
  },
  {
    "path": "tests/fixtures/parse/object.ini",
    "content": "[viewBag]\ncode = \"signin-snippet\"\nname = \"Sign in snippet\"\nproperties[type] = \"string\"\nproperties[title] = \"Redirection page\"\nproperties[default] = \"/clients\""
  },
  {
    "path": "tests/fixtures/parse/sections.ini",
    "content": "var1 = \"value 1\"\nvar2 = \"value 21\"\n\n[section]\nsectionVar1 = \"section value 1\"\nsectionVar2 = \"section value 2\"\n\n[section data]\nsectionVar3 = \"section value 3\"\nsectionVar4 = \"section value 4\"\n\n[emptysection]"
  },
  {
    "path": "tests/fixtures/parse/simple.ini",
    "content": "var1 = \"value 1\"\nvar2 = \"value 21\""
  },
  {
    "path": "tests/fixtures/parse/subsections.ini",
    "content": "var1 = \"value 1\"\nvar2 = \"value 21\"\n\n[section]\nsectionVar1 = \"section value 1\"\nsectionVar2 = \"section value 2\"\nsubsection[] = \"subsection value 1\"\nsubsection[] = \"subsection value 2\"\nsectionVar3 = \"section value 3\"\n\n[section data]\nsectionVar3 = \"section value 3\"\nsectionVar4 = \"section value 4\"\nsubsection[] = \"subsection value 1\"\nsubsection[] = \"subsection value 2\""
  },
  {
    "path": "tests/phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit backupGlobals=\"false\"\n         backupStaticAttributes=\"false\"\n         bootstrap=\"../vendor/autoload.php\"\n         colors=\"true\"\n         convertErrorsToExceptions=\"true\"\n         convertNoticesToExceptions=\"true\"\n         convertWarningsToExceptions=\"true\"\n         processIsolation=\"false\"\n         stopOnFailure=\"false\"\n         syntaxCheck=\"false\"\n>\n    <testsuites>\n        <testsuite name=\"October Rain Test Suite\">\n            <directory>./</directory>\n        </testsuite>\n    </testsuites>\n</phpunit>"
  }
]