├── LICENSE ├── README.md ├── SECURITY.md ├── composer.json └── src ├── AbstractApplication.php ├── AbstractWebApplication.php ├── ApplicationEvents.php ├── ApplicationInterface.php ├── ConfigurationAwareApplicationInterface.php ├── Controller ├── ContainerControllerResolver.php ├── ControllerResolver.php └── ControllerResolverInterface.php ├── Event ├── ApplicationErrorEvent.php └── ApplicationEvent.php ├── Exception └── UnableToWriteBody.php ├── SessionAwareWebApplicationInterface.php ├── SessionAwareWebApplicationTrait.php ├── Web └── WebClient.php ├── WebApplication.php └── WebApplicationInterface.php /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License 307 | along with this program; if not, write to the Free Software 308 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 309 | 310 | 311 | Also add information on how to contact you by electronic and paper mail. 312 | 313 | If the program is interactive, make it output a short notice like this 314 | when it starts in an interactive mode: 315 | 316 | Gnomovision version 69, Copyright (C) year name of author 317 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 318 | This is free software, and you are welcome to redistribute it 319 | under certain conditions; type `show c' for details. 320 | 321 | The hypothetical commands `show w' and `show c' should show the appropriate 322 | parts of the General Public License. Of course, the commands you use may 323 | be called something other than `show w' and `show c'; they could even be 324 | mouse-clicks or menu items--whatever suits your program. 325 | 326 | You should also get your employer (if you work as a programmer) or your 327 | school, if any, to sign a "copyright disclaimer" for the program, if 328 | necessary. Here is a sample; alter the names: 329 | 330 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 331 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 332 | 333 | , 1 April 1989 334 | Ty Coon, President of Vice 335 | 336 | This General Public License does not permit incorporating your program into 337 | proprietary programs. If your program is a subroutine library, you may 338 | consider it more useful to permit linking proprietary applications with the 339 | library. If this is what you want to do, use the GNU Library General 340 | Public License instead of this License. 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # The Application Package [![Build Status](https://ci.joomla.org/api/badges/joomla-framework/application/status.svg?ref=refs/heads/3.x-dev)](https://ci.joomla.org/joomla-framework/application) 2 | 3 | [![Latest Stable Version](https://poser.pugx.org/joomla/application/v/stable.svg)](https://packagist.org/packages/joomla/application) 4 | [![Total Downloads](https://poser.pugx.org/joomla/application/downloads.svg)](https://packagist.org/packages/joomla/application) 5 | [![Latest Unstable Version](https://poser.pugx.org/joomla/application/v/unstable.svg)](https://packagist.org/packages/joomla/application) 6 | [![License](https://poser.pugx.org/joomla/application/license.svg)](https://packagist.org/packages/joomla/application) 7 | 8 | ## Initialising Applications 9 | 10 | `AbstractApplication` implements an `initialise` method that is called at the end of the constructor. This method is intended to be overridden in derived classes as needed by the developer. 11 | 12 | If you are overriding the `__construct` method in your application class, remember to call the parent constructor last. 13 | 14 | ```php 15 | use Joomla\Application\AbstractApplication; 16 | use Joomla\Input\Input; 17 | use Joomla\Registry\Registry; 18 | 19 | class MyApplication extends AbstractApplication 20 | { 21 | /** 22 | * Customer constructor for my application class. 23 | * 24 | * @param Input $input 25 | * @param Registry $config 26 | * 27 | * @since 1.0 28 | */ 29 | public function __construct(Input $input = null, Registry $config = null, Foo $foo) 30 | { 31 | // Do some extra assignment. 32 | $this->foo = $foo; 33 | 34 | // Call the parent constructor last of all. 35 | parent::__construct($input, $config); 36 | } 37 | 38 | /** 39 | * Method to run the application routines. 40 | * 41 | * @return void 42 | * 43 | * @since 1.0 44 | */ 45 | protected function doExecute() 46 | { 47 | try 48 | { 49 | // Do stuff. 50 | } 51 | catch(\Exception $e) 52 | { 53 | // Set status header of exception code and response body of exception message 54 | $this->setHeader('status', $e->getCode() ?: 500); 55 | $this->setBody($e->getMessage()); 56 | } 57 | } 58 | 59 | /** 60 | * Custom initialisation for my application. 61 | * 62 | * @return void 63 | * 64 | * @since 1.0 65 | */ 66 | protected function initialise() 67 | { 68 | // Do stuff. 69 | // Note that configuration has been loaded. 70 | } 71 | } 72 | 73 | ``` 74 | 75 | ## Logging within Applications 76 | 77 | `AbstractApplication` implements the `Psr\Log\LoggerAwareInterface` so is ready for intergrating with an logging package that supports that standard. 78 | 79 | The following example shows how you could set up logging in your application using `initialise` method from `AbstractApplication`. 80 | 81 | ```php 82 | use Joomla\Application\AbstractApplication; 83 | use Monolog\Logger; 84 | use Monolog\Handler\NullHandler; 85 | use Monolog\Handler\StreamHandler; 86 | 87 | class MyApplication extends AbstractApplication 88 | { 89 | /** 90 | * Custom initialisation for my application. 91 | * 92 | * Note that configuration has been loaded. 93 | * 94 | * @return void 95 | * 96 | * @since 1.0 97 | */ 98 | protected function initialise() 99 | { 100 | // Get the file logging path from configuration. 101 | $logPath = $this->get('logger.path'); 102 | $log = new Logger('MyApp'); 103 | 104 | if ($logPath) 105 | { 106 | // If the log path is set, configure a file logger. 107 | $log->pushHandler(new StreamHandler($logPath, Logger::WARNING); 108 | } 109 | else 110 | { 111 | // If the log path is not set, just use a null logger. 112 | $log->pushHandler(new NullHandler, Logger::WARNING); 113 | } 114 | 115 | $this->setLogger($logger); 116 | } 117 | } 118 | 119 | ``` 120 | 121 | The logger variable is private so you must use the `getLogger` method to access it. If a logger has not been initialised, the `getLogger` method will throw an exception. 122 | 123 | To check if the logger has been set, use the `hasLogger` method. This will return `true` if the logger has been set. 124 | 125 | Consider the following example: 126 | 127 | ```php 128 | use Joomla\Application\AbstractApplication; 129 | 130 | class MyApplication extends AbstractApplication 131 | { 132 | protected function doExecute() 133 | { 134 | // In this case, we always want the logger set. 135 | $this->getLogger()->logInfo('Performed this {task}', array('task' => $task)); 136 | 137 | // Or, in this case logging is optional, so we check if the logger is set first. 138 | if ($this->get('debug') && $this->hasLogger()) 139 | { 140 | $this->getLogger()->logDebug('Performed {task}', array('task' => $task)); 141 | } 142 | } 143 | } 144 | ``` 145 | 146 | ## Mocking the Application Package 147 | 148 | For more complicated mocking where you need to similate real behaviour, you can use the `Application\Tests\Mocker` class to create robust mock objects. 149 | 150 | There are three mocking methods available: 151 | 152 | 1. `createMockBase` will create a mock for `AbstractApplication`. 153 | 2. `createMockCli` will create a mock for `AbstractCliApplication`. 154 | 3. `createMockWeb` will create a mock for `AbstractWebApplication`. 155 | 156 | ```php 157 | use Joomla\Application\Tests\Mocker as AppMocker; 158 | 159 | class MyTest extends \PHPUnit_Framework_TestCase 160 | { 161 | private $instance; 162 | 163 | protected function setUp() 164 | { 165 | parent::setUp(); 166 | 167 | // Create the mock input object. 168 | $appMocker = new AppMocker($this); 169 | $mockApp = $appMocker->createMockWeb(); 170 | 171 | // Create the test instance injecting the mock dependency. 172 | $this->instance = new MyClass($mockApp); 173 | } 174 | } 175 | ``` 176 | 177 | The `createMockWeb` method will return a mock with the following methods mocked to roughly simulate real behaviour albeit with reduced functionality: 178 | 179 | * `appendBody($content)` 180 | * `get($name [, $default])` 181 | * `getBody([$asArray])` 182 | * `getHeaders()` 183 | * `prependBody($content)` 184 | * `set($name, $value)` 185 | * `setBody($content)` 186 | * `setHeader($name, $value [, $replace])` 187 | 188 | You can provide customised implementations these methods by creating the following methods in your test class respectively: 189 | 190 | * `mockWebAppendBody` 191 | * `mockWebGet` 192 | * `mockWebGetBody` 193 | * `mockWebGetHeaders` 194 | * `mockWebSet` 195 | * `mockWebSetBody` 196 | * `mockWebSetHeader` 197 | 198 | 199 | ## Web Application 200 | 201 | ### Configuration options 202 | 203 | The `AbstractWebApplication` sets following application configuration: 204 | 205 | - Execution datetime and timestamp 206 | - `execution.datetime` - Execution datetime 207 | - `execution.timestamp` - Execution timestamp 208 | 209 | - URIs 210 | - `uri.request` - The request URI 211 | - `uri.base.full` - full URI 212 | - `uri.base.host` - URI host 213 | - `uri.base.path` - URI path 214 | - `uri.route` - Extended (non-base) part of the request URI 215 | - `uri.media.full` - full media URI 216 | - `uri.media.path` - relative media URI 217 | 218 | and uses following ones during object construction: 219 | 220 | - `gzip` to compress the output 221 | - `site_uri` to see if an explicit base URI has been set 222 | (helpful when chaining request uri using mod_rewrite) 223 | - `media_uri` to get an explicitly set media URI (relative values are appended to `uri.base` ). 224 | If it's not set explicitly, it defaults to a `media/` path of `uri.base`. 225 | 226 | #### The `setHeader` method 227 | __Accepted parameters__ 228 | 229 | - `$name` - The name of the header to set. 230 | - `$value` - The value of the header to set. 231 | - `$replace` - True to replace any headers with the same name. 232 | 233 | Example: Using `WebApplication::setHeader` to set a status header. 234 | 235 | ```PHP 236 | $app->setHeader('status', '401 Auhtorization required', true); 237 | ``` 238 | 239 | Will result in response containing header 240 | ``` 241 | Status Code: 401 Authorization required 242 | ``` 243 | 244 | ## Command Line Applications 245 | 246 | The Joomla Framework provides an application class for making command line applications. 247 | 248 | An example command line application skeleton: 249 | 250 | ```php 251 | use Joomla\Application\AbstractCliApplication; 252 | 253 | // Bootstrap the autoloader (adjust path as appropriate to your situation). 254 | require_once __DIR__ . '/../vendor/autoload.php'; 255 | 256 | class MyCli extends AbstractCliApplication 257 | { 258 | protected function doExecute() 259 | { 260 | // Output string 261 | $this->out('It works'); 262 | 263 | // Get user input 264 | $this->out('What is your name? ', false); 265 | 266 | $userInput = $this->in(); 267 | $this->out('Hello ' . $userInput); 268 | } 269 | } 270 | 271 | $app = new MyCli; 272 | $app->execute(); 273 | 274 | ``` 275 | 276 | ### Colors for CLI Applications 277 | 278 | It is possible to use colors on an ANSI enabled terminal. 279 | 280 | ```php 281 | use Joomla\Application\AbstractCliApplication; 282 | 283 | class MyCli extends AbstractCliApplication 284 | { 285 | protected function doExecute() 286 | { 287 | // Green text 288 | $this->out('foo'); 289 | 290 | // Yellow text 291 | $this->out('foo'); 292 | 293 | // Black text on a cyan background 294 | $this->out('foo'); 295 | 296 | // White text on a red background 297 | $this->out('foo'); 298 | } 299 | } 300 | ``` 301 | 302 | You can also create your own styles. 303 | 304 | ```php 305 | use Joomla\Application\AbstractCliApplication; 306 | use Joomla\Application\Cli\Colorstyle; 307 | 308 | class MyCli extends AbstractCliApplication 309 | { 310 | /** 311 | * Override to initialise the colour styles. 312 | * 313 | * @return void 314 | * 315 | * @since 1.0 316 | */ 317 | protected function initialise() 318 | { 319 | $style = new Colorstyle('yellow', 'red', array('bold', 'blink')); 320 | $this->getOutput()->addStyle('fire', $style); 321 | } 322 | 323 | protected function doExecute() 324 | { 325 | $this->out('foo'); 326 | } 327 | } 328 | 329 | ``` 330 | 331 | Available foreground and background colors are: black, red, green, yellow, blue, magenta, cyan and white. 332 | 333 | And available options are: bold, underscore, blink and reverse. 334 | 335 | You can also set these colors and options inside the tagname: 336 | 337 | ```php 338 | use Joomla\Application\AbstractCliApplication; 339 | 340 | class MyCli extends AbstractCliApplication 341 | { 342 | protected function doExecute() 343 | { 344 | // Green text 345 | $this->out('foo'); 346 | 347 | // Black text on a cyan background 348 | $this->out('foo'); 349 | 350 | // Bold text on a yellow background 351 | $this->out('foo'); 352 | } 353 | } 354 | ``` 355 | 356 | ## Installation via Composer 357 | 358 | Add `"joomla/application": "~3.0"` to the require block in your composer.json and then run `composer install`. 359 | 360 | ```json 361 | { 362 | "require": { 363 | "joomla/application": "~3.0" 364 | } 365 | } 366 | ``` 367 | 368 | Alternatively, you can simply run the following from the command line: 369 | 370 | ```sh 371 | composer require joomla/application "~3.0" 372 | ``` 373 | 374 | If you want to include the test sources, use 375 | 376 | ```sh 377 | composer require --prefer-source joomla/application "~3.0" 378 | ``` 379 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | These versions are currently being supported with security updates: 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 3.x.x | :white_check_mark: | 10 | | 2.0.x | :white_check_mark: | 11 | | 1.9.x | :x: | 12 | | < 1.9 | :x: | 13 | 14 | ## Reporting a Vulnerability 15 | 16 | To report a security issue in the core Joomla! CMS or Framework, or with a joomla.org website, please submit 17 | [the form on our portal](https://developer.joomla.org/security/contact-the-team.html) containing as much detail 18 | as possible about the issue. Additional information about our security team and their processes may be found on 19 | our [Security page](https://developer.joomla.org/security.html). 20 | 21 | To report an issue in a Joomla! extension, please submit it to the [Vulnerable Extensions List](https://vel.joomla.org/submit-vel). 22 | 23 | For support with a site which has been attacked, please visit the [Joomla! Forum](https://forum.joomla.org/viewforum.php?f=714). 24 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "joomla/application", 3 | "description": "Joomla Application Package", 4 | "type": "library", 5 | "keywords": [ 6 | "joomla", 7 | "framework", 8 | "application", 9 | "joomla-package" 10 | ], 11 | "homepage": "https://github.com/joomla-framework/application", 12 | "readme": "https://github.com/joomla-framework/application/README.md", 13 | "license": "GPL-2.0-or-later", 14 | "authors": [ 15 | { 16 | "name": "The Joomla! Project", 17 | "homepage": "https://framework.joomla.org/" 18 | } 19 | ], 20 | "support": { 21 | "issues": "https://github.com/joomla-framework/application/issues", 22 | "forum": "https://groups.google.com/g/joomla-dev-framework", 23 | "wiki": "https://github.com/joomla-framework/application/wiki", 24 | "docs": "https://developer.joomla.org/framework/documentation.html", 25 | "source": "https://github.com/joomla-framework/application" 26 | }, 27 | "funding": [ 28 | { 29 | "type": "github", 30 | "url": "https://github.com/sponsors/joomla" 31 | }, 32 | { 33 | "type": "custom", 34 | "url": "https://community.joomla.org/sponsorship-campaigns.html" 35 | } 36 | ], 37 | "require": { 38 | "php": "^8.1.0", 39 | "psr/log": "^1.0|^2.0|^3.0", 40 | "psr/http-message": "^1.0", 41 | "joomla/event": "^3.0", 42 | "joomla/registry": "^3.0", 43 | "laminas/laminas-diactoros": "^2.24.0", 44 | "symfony/deprecation-contracts": "^2|^3" 45 | }, 46 | "require-dev": { 47 | "ext-json": "*", 48 | "joomla/controller": "^3.0", 49 | "joomla/di": "^3.0", 50 | "joomla/input": "^3.0", 51 | "joomla/router": "^3.0", 52 | "joomla/session": "^3.0", 53 | "joomla/test": "^3.0", 54 | "joomla/uri": "^3.0", 55 | "phpunit/phpunit": "^10.0", 56 | "symfony/phpunit-bridge": "^7.0", 57 | "squizlabs/php_codesniffer": "~3.10.2", 58 | "rector/rector": "^1.0", 59 | "phpstan/phpstan": "^1.10.7", 60 | "phan/phan": "^5.4" 61 | }, 62 | "suggest": { 63 | "ext-json": "To use JSON format, ext-json is required", 64 | "joomla/controller": "^3.0 To support resolving ControllerInterface objects in ControllerResolverInterface, install joomla/controller", 65 | "joomla/input": "^3.0 To use WebApplicationInterface, install joomla/input", 66 | "joomla/router": "^3.0 To use WebApplication or ControllerResolverInterface implementations, install joomla/router", 67 | "joomla/session": "^3.0 To use SessionAwareWebApplicationInterface, install joomla/session", 68 | "joomla/uri": "^3.0 To use AbstractWebApplication, install joomla/uri", 69 | "psr/container": "^1.0 To use the ContainerControllerResolver, install any PSR-11 compatible container" 70 | }, 71 | "autoload": { 72 | "psr-4": { 73 | "Joomla\\Application\\": "src/" 74 | } 75 | }, 76 | "autoload-dev": { 77 | "psr-4": { 78 | "Joomla\\Application\\Tests\\": "Tests/" 79 | } 80 | }, 81 | "minimum-stability": "dev", 82 | "prefer-stable": true, 83 | "scripts": { 84 | "test": "vendor/bin/phpunit", 85 | "testdox": "vendor/bin/phpunit --testdox", 86 | "coverage": "vendor/bin/phpunit --coverage-html build/coverage", 87 | "sign": [ 88 | "drone jsonnet --stream", 89 | "drone sign joomla-framework/application --save" 90 | ], 91 | "style": [ 92 | "vendor/bin/phpcbf --report=full --extensions=php --standard=PSR12 src/", 93 | "vendor/bin/phpcs --report=full --extensions=php --standard=PSR12 src/" 94 | ] 95 | }, 96 | "scripts-descriptions": { 97 | "test": "Run the PHPUnit tests", 98 | "testdox": "Run the PHPUnit tests and output the result in testdox format", 99 | "coverage": "Run the PHPUnit tests and write an HTML coverage report to build/coverage", 100 | "sign": "Compile .drone.jsonnet and sign the resulting YML file. Make sure to have set DRONE_SERVER and DRONE_TOKEN properly.", 101 | "style": "Fix any style issues that can be fixed automatically and run a check afterwards to list issues that could not be solved." 102 | }, 103 | "extra": { 104 | "branch-alias": { 105 | "dev-2.0-dev": "2.0-dev", 106 | "dev-3.x-dev": "3.0-dev" 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/AbstractApplication.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License version 2 or later; see LICENSE 8 | */ 9 | 10 | namespace Joomla\Application; 11 | 12 | use Joomla\Application\Event\ApplicationErrorEvent; 13 | use Joomla\Application\Event\ApplicationEvent; 14 | use Joomla\Event\DispatcherAwareInterface; 15 | use Joomla\Event\DispatcherAwareTrait; 16 | use Joomla\Event\EventInterface; 17 | use Joomla\Registry\Registry; 18 | use Psr\Log\LoggerAwareInterface; 19 | use Psr\Log\LoggerAwareTrait; 20 | use Psr\Log\LoggerInterface; 21 | use Psr\Log\NullLogger; 22 | 23 | /** 24 | * Joomla Framework Base Application Class 25 | * 26 | * @since 1.0.0 27 | */ 28 | abstract class AbstractApplication implements 29 | ConfigurationAwareApplicationInterface, 30 | LoggerAwareInterface, 31 | DispatcherAwareInterface 32 | { 33 | use LoggerAwareTrait; 34 | use DispatcherAwareTrait; 35 | 36 | /** 37 | * The application configuration object. 38 | * 39 | * @var Registry 40 | * @since 1.0.0 41 | */ 42 | protected $config; 43 | 44 | /** 45 | * Class constructor. 46 | * 47 | * @param Registry|null $config An optional argument to provide dependency injection for the 48 | * application's config object. If the argument is a Registry 49 | * object that object will become the application's config object, 50 | * otherwise a default config object is created. 51 | * 52 | * @since 1.0.0 53 | */ 54 | public function __construct(?Registry $config = null) 55 | { 56 | $this->config = $config ?: new Registry(); 57 | 58 | // Set the execution datetime and timestamp; 59 | $this->set('execution.datetime', \gmdate('Y-m-d H:i:s')); 60 | $this->set('execution.timestamp', \time()); 61 | $this->set('execution.microtimestamp', \microtime(true)); 62 | 63 | $this->initialise(); 64 | } 65 | 66 | /** 67 | * Method to close the application. 68 | * 69 | * @param integer $code The exit code (optional; default is 0). 70 | * 71 | * @return void 72 | * 73 | * @codeCoverageIgnore 74 | * @since 1.0.0 75 | */ 76 | public function close($code = 0) 77 | { 78 | exit($code); 79 | } 80 | 81 | /** 82 | * Dispatches an application event if the dispatcher has been set. 83 | * 84 | * @param string $eventName The event to dispatch. 85 | * @param EventInterface|null $event The event object. 86 | * 87 | * @return EventInterface|null The dispatched event or null if no dispatcher is set 88 | * 89 | * @since 2.0.0 90 | */ 91 | protected function dispatchEvent(string $eventName, ?EventInterface $event = null): ?EventInterface 92 | { 93 | try { 94 | $dispatcher = $this->getDispatcher(); 95 | } catch (\UnexpectedValueException $exception) { 96 | return null; 97 | } 98 | 99 | return $dispatcher->dispatch($eventName, $event ?: new ApplicationEvent($eventName, $this)); 100 | } 101 | 102 | /** 103 | * Method to run the application routines. 104 | * 105 | * Most likely you will want to instantiate a controller and execute it, or perform some sort of task directly. 106 | * 107 | * @return mixed 108 | * 109 | * @since 1.0.0 110 | */ 111 | abstract protected function doExecute(); 112 | 113 | /** 114 | * Execute the application. 115 | * 116 | * @return void 117 | * 118 | * @since 1.0.0 119 | */ 120 | public function execute() 121 | { 122 | try { 123 | $this->dispatchEvent(ApplicationEvents::BEFORE_EXECUTE); 124 | 125 | // Perform application routines. 126 | $this->doExecute(); 127 | 128 | $this->dispatchEvent(ApplicationEvents::AFTER_EXECUTE); 129 | } catch (\Throwable $throwable) { 130 | $this->dispatchEvent(ApplicationEvents::ERROR, new ApplicationErrorEvent($throwable, $this)); 131 | } 132 | } 133 | 134 | /** 135 | * Returns a property of the object or the default value if the property is not set. 136 | * 137 | * @param string $key The name of the property. 138 | * @param mixed $default The default value (optional) if none is set. 139 | * 140 | * @return mixed The value of the configuration. 141 | * 142 | * @since 1.0.0 143 | */ 144 | public function get($key, $default = null) 145 | { 146 | return $this->config->get($key, $default); 147 | } 148 | 149 | /** 150 | * Get the logger. 151 | * 152 | * @return LoggerInterface 153 | * 154 | * @since 1.0.0 155 | */ 156 | public function getLogger() 157 | { 158 | // If a logger hasn't been set, use NullLogger 159 | if (!($this->logger instanceof LoggerInterface)) { 160 | $this->setLogger(new NullLogger()); 161 | } 162 | 163 | return $this->logger; 164 | } 165 | 166 | /** 167 | * Custom initialisation method. 168 | * 169 | * Called at the end of the AbstractApplication::__construct method. 170 | * This is for developers to inject initialisation code for their application classes. 171 | * 172 | * @return void 173 | * 174 | * @codeCoverageIgnore 175 | * @since 1.0.0 176 | */ 177 | protected function initialise() 178 | { 179 | } 180 | 181 | /** 182 | * Modifies a property of the object, creating it if it does not already exist. 183 | * 184 | * @param string $key The name of the property. 185 | * @param mixed $value The value of the property to set (optional). 186 | * 187 | * @return mixed Previous value of the property 188 | * 189 | * @since 1.0.0 190 | */ 191 | public function set($key, $value = null) 192 | { 193 | $previous = $this->config->get($key); 194 | $this->config->set($key, $value); 195 | 196 | return $previous; 197 | } 198 | 199 | /** 200 | * Sets the configuration for the application. 201 | * 202 | * @param Registry $config A registry object holding the configuration. 203 | * 204 | * @return $this 205 | * 206 | * @since 1.0.0 207 | */ 208 | public function setConfiguration(Registry $config) 209 | { 210 | $this->config = $config; 211 | 212 | return $this; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/AbstractWebApplication.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License version 2 or later; see LICENSE 8 | */ 9 | 10 | namespace Joomla\Application; 11 | 12 | use Joomla\Application\Event\ApplicationErrorEvent; 13 | use Joomla\Application\Exception\UnableToWriteBody; 14 | use Joomla\Application\Web\WebClient; 15 | use Joomla\Input\Input; 16 | use Joomla\Registry\Registry; 17 | use Joomla\Uri\Uri; 18 | use Laminas\Diactoros\Response; 19 | use Laminas\Diactoros\Stream; 20 | use Psr\Http\Message\ResponseInterface; 21 | 22 | /** 23 | * Base class for a Joomla! Web application. 24 | * 25 | * @since 1.0.0 26 | * 27 | * @property-read Input $input The application input object 28 | */ 29 | abstract class AbstractWebApplication extends AbstractApplication implements WebApplicationInterface 30 | { 31 | /** 32 | * The application input object. 33 | * 34 | * @var Input 35 | * @since 1.0.0 36 | */ 37 | protected $input; 38 | 39 | /** 40 | * Character encoding string. 41 | * 42 | * @var string 43 | * @since 1.0.0 44 | */ 45 | public $charSet = 'utf-8'; 46 | 47 | /** 48 | * Response mime type. 49 | * 50 | * @var string 51 | * @since 1.0.0 52 | */ 53 | public $mimeType = 'text/html'; 54 | 55 | /** 56 | * HTTP protocol version. 57 | * 58 | * @var string 59 | * @since 1.9.0 60 | */ 61 | public $httpVersion = '1.1'; 62 | 63 | /** 64 | * The body modified date for response headers. 65 | * 66 | * @var \DateTime 67 | * @since 1.0.0 68 | */ 69 | public $modifiedDate; 70 | 71 | /** 72 | * The application client object. 73 | * 74 | * @var Web\WebClient 75 | * @since 1.0.0 76 | */ 77 | public $client; 78 | 79 | /** 80 | * The application response object. 81 | * 82 | * @var ResponseInterface 83 | * @since 1.0.0 84 | */ 85 | protected $response; 86 | 87 | /** 88 | * Is caching enabled? 89 | * 90 | * @var boolean 91 | * @since 2.0.0 92 | */ 93 | private $cacheable = false; 94 | 95 | /** 96 | * A map of integer HTTP response codes to the full HTTP Status for the headers. 97 | * 98 | * @var array 99 | * @since 1.6.0 100 | * @link https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml 101 | */ 102 | private $responseMap = [ 103 | 100 => 'HTTP/{version} 100 Continue', 104 | 101 => 'HTTP/{version} 101 Switching Protocols', 105 | 102 => 'HTTP/{version} 102 Processing', 106 | 200 => 'HTTP/{version} 200 OK', 107 | 201 => 'HTTP/{version} 201 Created', 108 | 202 => 'HTTP/{version} 202 Accepted', 109 | 203 => 'HTTP/{version} 203 Non-Authoritative Information', 110 | 204 => 'HTTP/{version} 204 No Content', 111 | 205 => 'HTTP/{version} 205 Reset Content', 112 | 206 => 'HTTP/{version} 206 Partial Content', 113 | 207 => 'HTTP/{version} 207 Multi-Status', 114 | 208 => 'HTTP/{version} 208 Already Reported', 115 | 226 => 'HTTP/{version} 226 IM Used', 116 | 300 => 'HTTP/{version} 300 Multiple Choices', 117 | 301 => 'HTTP/{version} 301 Moved Permanently', 118 | 302 => 'HTTP/{version} 302 Found', 119 | 303 => 'HTTP/{version} 303 See other', 120 | 304 => 'HTTP/{version} 304 Not Modified', 121 | 305 => 'HTTP/{version} 305 Use Proxy', 122 | 306 => 'HTTP/{version} 306 (Unused)', 123 | 307 => 'HTTP/{version} 307 Temporary Redirect', 124 | 308 => 'HTTP/{version} 308 Permanent Redirect', 125 | 400 => 'HTTP/{version} 400 Bad Request', 126 | 401 => 'HTTP/{version} 401 Unauthorized', 127 | 402 => 'HTTP/{version} 402 Payment Required', 128 | 403 => 'HTTP/{version} 403 Forbidden', 129 | 404 => 'HTTP/{version} 404 Not Found', 130 | 405 => 'HTTP/{version} 405 Method Not Allowed', 131 | 406 => 'HTTP/{version} 406 Not Acceptable', 132 | 407 => 'HTTP/{version} 407 Proxy Authentication Required', 133 | 408 => 'HTTP/{version} 408 Request Timeout', 134 | 409 => 'HTTP/{version} 409 Conflict', 135 | 410 => 'HTTP/{version} 410 Gone', 136 | 411 => 'HTTP/{version} 411 Length Required', 137 | 412 => 'HTTP/{version} 412 Precondition Failed', 138 | 413 => 'HTTP/{version} 413 Payload Too Large', 139 | 414 => 'HTTP/{version} 414 URI Too Long', 140 | 415 => 'HTTP/{version} 415 Unsupported Media Type', 141 | 416 => 'HTTP/{version} 416 Range Not Satisfiable', 142 | 417 => 'HTTP/{version} 417 Expectation Failed', 143 | 418 => 'HTTP/{version} 418 I\'m a teapot', 144 | 421 => 'HTTP/{version} 421 Misdirected Request', 145 | 422 => 'HTTP/{version} 422 Unprocessable Entity', 146 | 423 => 'HTTP/{version} 423 Locked', 147 | 424 => 'HTTP/{version} 424 Failed Dependency', 148 | 426 => 'HTTP/{version} 426 Upgrade Required', 149 | 428 => 'HTTP/{version} 428 Precondition Required', 150 | 429 => 'HTTP/{version} 429 Too Many Requests', 151 | 431 => 'HTTP/{version} 431 Request Header Fields Too Large', 152 | 451 => 'HTTP/{version} 451 Unavailable For Legal Reasons', 153 | 500 => 'HTTP/{version} 500 Internal Server Error', 154 | 501 => 'HTTP/{version} 501 Not Implemented', 155 | 502 => 'HTTP/{version} 502 Bad Gateway', 156 | 503 => 'HTTP/{version} 503 Service Unavailable', 157 | 504 => 'HTTP/{version} 504 Gateway Timeout', 158 | 505 => 'HTTP/{version} 505 HTTP Version Not Supported', 159 | 506 => 'HTTP/{version} 506 Variant Also Negotiates', 160 | 507 => 'HTTP/{version} 507 Insufficient Storage', 161 | 508 => 'HTTP/{version} 508 Loop Detected', 162 | 510 => 'HTTP/{version} 510 Not Extended', 163 | 511 => 'HTTP/{version} 511 Network Authentication Required', 164 | ]; 165 | 166 | /** 167 | * Class constructor. 168 | * 169 | * @param Input|null $input An optional argument to provide dependency 170 | * injection for the application's input object. If 171 | * the argument is an Input object that object will 172 | * become the application's input object, otherwise a 173 | * default input object is created. 174 | * @param Registry|null $config An optional argument to provide dependency 175 | * injection for the application's config object. If 176 | * the argument is a Registry object that object will 177 | * become the application's config object, otherwise a 178 | * default config object is created. 179 | * @param WebClient|null $client An optional argument to provide dependency 180 | * injection for the application's client object. If 181 | * the argument is a Web\WebClient object that object 182 | * will become the application's client object, 183 | * otherwise a default client object is created. 184 | * @param ResponseInterface|null $response An optional argument to provide dependency 185 | * injection for the application's response object. 186 | * If the argument is a ResponseInterface object that 187 | * object will become the application's response 188 | * object, otherwise a default response object is 189 | * created. 190 | * 191 | * @since 1.0.0 192 | */ 193 | public function __construct( 194 | ?Input $input = null, 195 | ?Registry $config = null, 196 | ?WebClient $client = null, 197 | ?ResponseInterface $response = null 198 | ) { 199 | $this->input = $input ?: new Input(); 200 | $this->client = $client ?: new WebClient(); 201 | 202 | // Setup the response object. 203 | if (!$response) { 204 | $response = new Response(); 205 | } 206 | 207 | $this->setResponse($response); 208 | 209 | // Call the constructor as late as possible (it runs `initialise`). 210 | parent::__construct($config); 211 | 212 | // Set the system URIs. 213 | $this->loadSystemUris(); 214 | } 215 | 216 | /** 217 | * Magic method to access properties of the application. 218 | * 219 | * @param string $name The name of the property. 220 | * 221 | * @return Input|null A value if the property name is valid, null otherwise. 222 | * 223 | * @since 2.0.0 224 | * @deprecated 3.0 This is a B/C proxy for deprecated read accesses 225 | */ 226 | public function __get($name) 227 | { 228 | switch ($name) { 229 | case 'input': 230 | \trigger_deprecation( 231 | 'joomla/application', 232 | '2.0.0', 233 | 'Accessing the input property of %s is deprecated, use the %s::getInput() method instead.', 234 | self::class, 235 | self::class 236 | ); 237 | 238 | return $this->getInput(); 239 | 240 | default: 241 | $trace = \debug_backtrace(); 242 | \trigger_error( 243 | \sprintf( 244 | 'Undefined property via __get(): %1$s in %2$s on line %3$s', 245 | $name, 246 | $trace[0]['file'], 247 | $trace[0]['line'] 248 | ), 249 | E_USER_NOTICE 250 | ); 251 | 252 | return null; 253 | } 254 | } 255 | 256 | /** 257 | * Execute the application. 258 | * 259 | * @return void 260 | * 261 | * @since 1.0.0 262 | */ 263 | public function execute() 264 | { 265 | try { 266 | $this->dispatchEvent(ApplicationEvents::BEFORE_EXECUTE); 267 | 268 | // Perform application routines. 269 | $this->doExecute(); 270 | 271 | $this->dispatchEvent(ApplicationEvents::AFTER_EXECUTE); 272 | 273 | // If gzip compression is enabled in configuration and the server is compliant, compress the output. 274 | if ( 275 | $this->get('gzip') 276 | && !\ini_get('zlib.output_compression') 277 | && (\ini_get('output_handler') != 'ob_gzhandler') 278 | ) { 279 | $this->compress(); 280 | } 281 | } catch (\Throwable $throwable) { 282 | $this->dispatchEvent(ApplicationEvents::ERROR, new ApplicationErrorEvent($throwable, $this)); 283 | } 284 | 285 | $this->dispatchEvent(ApplicationEvents::BEFORE_RESPOND); 286 | 287 | // Send the application response. 288 | $this->respond(); 289 | 290 | $this->dispatchEvent(ApplicationEvents::AFTER_RESPOND); 291 | } 292 | 293 | /** 294 | * Checks the accept encoding of the browser and compresses the data before sending it to the client if possible. 295 | * 296 | * @return void 297 | * 298 | * @since 1.0.0 299 | */ 300 | protected function compress() 301 | { 302 | // Supported compression encodings. 303 | $supported = [ 304 | 'x-gzip' => 'gz', 305 | 'gzip' => 'gz', 306 | 'deflate' => 'deflate', 307 | ]; 308 | 309 | // Get the supported encoding. 310 | $encodings = \array_intersect($this->client->encodings, \array_keys($supported)); 311 | 312 | // If no supported encoding is detected do nothing and return. 313 | if (empty($encodings)) { 314 | return; 315 | } 316 | 317 | // Verify that headers have not yet been sent, and that our connection is still alive. 318 | if ($this->checkHeadersSent() || !$this->checkConnectionAlive()) { 319 | return; 320 | } 321 | 322 | // Iterate through the encodings and attempt to compress the data using any found supported encodings. 323 | foreach ($encodings as $encoding) { 324 | if (($supported[$encoding] == 'gz') || ($supported[$encoding] == 'deflate')) { 325 | // Verify that the server supports gzip compression before we attempt to gzip encode the data. 326 | // @codeCoverageIgnoreStart 327 | if (!\extension_loaded('zlib') || \ini_get('zlib.output_compression')) { 328 | continue; 329 | } 330 | 331 | // @codeCoverageIgnoreEnd 332 | 333 | // Attempt to gzip encode the data with an optimal level 4. 334 | $data = $this->getBody(); 335 | $gzdata = \gzencode($data, 4, ($supported[$encoding] == 'gz') ? FORCE_GZIP : FORCE_DEFLATE); 336 | 337 | // If there was a problem encoding the data just try the next encoding scheme. 338 | // @codeCoverageIgnoreStart 339 | if ($gzdata === false) { 340 | continue; 341 | } 342 | 343 | // @codeCoverageIgnoreEnd 344 | 345 | // Set the encoding headers. 346 | $this->setHeader('Content-Encoding', $encoding); 347 | $this->setHeader('Vary', 'Accept-Encoding'); 348 | 349 | // Replace the output with the encoded data. 350 | $this->setBody($gzdata); 351 | 352 | // Compression complete, let's break out of the loop. 353 | break; 354 | } 355 | } 356 | } 357 | 358 | /** 359 | * Method to send the application response to the client. All headers will be sent prior to the main application 360 | * output data. 361 | * 362 | * @return void 363 | * 364 | * @since 1.0.0 365 | */ 366 | protected function respond() 367 | { 368 | // Send the content-type header. 369 | if (!$this->getResponse()->hasHeader('Content-Type')) { 370 | $this->setHeader('Content-Type', $this->mimeType . '; charset=' . $this->charSet); 371 | } 372 | 373 | // If the response is set to uncachable, et some appropriate headers so browsers don't cache the response. 374 | if (!$this->allowCache()) { 375 | // Expires in the past. 376 | $this->setHeader('Expires', 'Wed, 17 Aug 2005 00:00:00 GMT', true); 377 | 378 | // Always modified. 379 | $this->setHeader('Last-Modified', \gmdate('D, d M Y H:i:s') . ' GMT', true); 380 | $this->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0', false); 381 | 382 | // HTTP 1.0 383 | $this->setHeader('Pragma', 'no-cache'); 384 | } else { 385 | // Expires. 386 | if (!$this->getResponse()->hasHeader('Expires')) { 387 | $this->setHeader('Expires', \gmdate('D, d M Y H:i:s', \time() + 900) . ' GMT'); 388 | } 389 | 390 | // Last modified. 391 | if (!$this->getResponse()->hasHeader('Last-Modified') && $this->modifiedDate instanceof \DateTime) { 392 | $this->modifiedDate->setTimezone(new \DateTimeZone('UTC')); 393 | $this->setHeader('Last-Modified', $this->modifiedDate->format('D, d M Y H:i:s') . ' GMT'); 394 | } 395 | } 396 | 397 | // Make sure there is a status header already otherwise generate it from the response 398 | if (!$this->getResponse()->hasHeader('Status')) { 399 | $this->setHeader('Status', (string) $this->getResponse()->getStatusCode()); 400 | } 401 | 402 | $this->sendHeaders(); 403 | 404 | echo $this->getBody(); 405 | } 406 | 407 | /** 408 | * Method to get the application input object. 409 | * 410 | * @return Input 411 | * 412 | * @since 2.0.0 413 | */ 414 | public function getInput(): Input 415 | { 416 | return $this->input; 417 | } 418 | 419 | /** 420 | * Redirect to another URL. 421 | * 422 | * If the headers have not been sent the redirect will be accomplished using a "301 Moved Permanently" or "303 See 423 | * Other" code in the header pointing to the new location. If the headers have already been sent this will be 424 | * accomplished using a JavaScript statement. 425 | * 426 | * @param string $url The URL to redirect to. Can only be http/https URL 427 | * @param integer|boolean $status The HTTP status code to be provided. 303 is assumed by default. 428 | * 429 | * @return void 430 | * 431 | * @throws \InvalidArgumentException 432 | * @since 1.0.0 433 | */ 434 | public function redirect($url, $status = 303) 435 | { 436 | // Check for relative internal links. 437 | if (\preg_match('#^index\.php#', $url)) { 438 | $url = $this->get('uri.base.full') . $url; 439 | } 440 | 441 | // Perform a basic sanity check to make sure we don't have any CRLF garbage. 442 | $url = \preg_split("/[\r\n]/", $url); 443 | $url = $url[0]; 444 | 445 | /* 446 | * Here we need to check and see if the URL is relative or absolute. Essentially, do we need to 447 | * prepend the URL with our base URL for a proper redirect. The rudimentary way we are looking 448 | * at this is to simply check whether or not the URL string has a valid scheme or not. 449 | */ 450 | if (!\preg_match('#^[a-z]+://#i', $url)) { 451 | // Get a Uri instance for the requested URI. 452 | $uri = new Uri($this->get('uri.request')); 453 | 454 | // Get a base URL to prepend from the requested URI. 455 | $prefix = $uri->toString(['scheme', 'user', 'pass', 'host', 'port']); 456 | 457 | // We just need the prefix since we have a path relative to the root. 458 | if ($url[0] == '/') { 459 | $url = $prefix . $url; 460 | } else { 461 | // It's relative to where we are now, so lets add that. 462 | $parts = \explode('/', $uri->toString(['path'])); 463 | \array_pop($parts); 464 | $path = \implode('/', $parts) . '/'; 465 | $url = $prefix . $path . $url; 466 | } 467 | } 468 | 469 | if ($this->checkHeadersSent()) { 470 | // If the headers have already been sent we need to send the redirect statement via JavaScript. 471 | echo '\n"; 472 | } elseif (($this->client->engine == WebClient::TRIDENT) && !static::isAscii($url)) { 473 | // We have to use a JavaScript redirect here because MSIE doesn't play nice with UTF-8 URLs. 474 | $html = ''; 475 | $html .= ''; 476 | $html .= ''; 477 | $html .= ''; 478 | 479 | echo $html; 480 | } else { 481 | // Check if we have a boolean for the status variable for compatibility with v1 of the framework 482 | // @deprecated 3.0 483 | if (\is_bool($status)) { 484 | \trigger_deprecation( 485 | 'joomla/application', 486 | '2.0.0', 487 | 'Passing a boolean value for the $status argument in %s() is deprecated,' 488 | . ' an integer should be passed instead.', 489 | __METHOD__ 490 | ); 491 | 492 | $status = $status ? 301 : 303; 493 | } 494 | 495 | if (!\is_int($status) && !$this->isRedirectState($status)) { 496 | throw new \InvalidArgumentException('You have not supplied a valid HTTP status code'); 497 | } 498 | 499 | // All other cases use the more efficient HTTP header for redirection. 500 | $this->setHeader('Status', (string) $status, true); 501 | $this->setHeader('Location', $url, true); 502 | } 503 | 504 | $this->dispatchEvent(ApplicationEvents::BEFORE_RESPOND); 505 | 506 | // Set appropriate headers 507 | $this->respond(); 508 | 509 | $this->dispatchEvent(ApplicationEvents::AFTER_RESPOND); 510 | 511 | // Close the application after the redirect. 512 | $this->close(); 513 | } 514 | 515 | /** 516 | * Set/get cachable state for the response. 517 | * 518 | * If $allow is set, sets the cachable state of the response. Always returns the current state. 519 | * 520 | * @param boolean $allow True to allow browser caching. 521 | * 522 | * @return boolean 523 | * 524 | * @since 1.0.0 525 | */ 526 | public function allowCache($allow = null) 527 | { 528 | if ($allow !== null) { 529 | $this->cacheable = (bool) $allow; 530 | } 531 | 532 | return $this->cacheable; 533 | } 534 | 535 | /** 536 | * Method to set a response header. 537 | * 538 | * If the replace flag is set then all headers with the given name will be replaced by the new one. 539 | * The headers are stored in an internal array to be sent when the site is sent to the browser. 540 | * 541 | * @param string $name The name of the header to set. 542 | * @param string $value The value of the header to set. 543 | * @param boolean $replace True to replace any headers with the same name. 544 | * 545 | * @return $this 546 | * 547 | * @since 1.0.0 548 | */ 549 | public function setHeader($name, $value, $replace = false) 550 | { 551 | // Sanitize the input values. 552 | $name = (string) $name; 553 | $value = (string) $value; 554 | $response = $this->getResponse(); 555 | 556 | // If the replace flag is set, unset all known headers with the given name. 557 | if ($replace && $response->hasHeader($name)) { 558 | $response = $response->withoutHeader($name); 559 | } 560 | 561 | // Add the header to the internal array. 562 | $this->setResponse($response->withAddedHeader($name, $value)); 563 | 564 | return $this; 565 | } 566 | 567 | /** 568 | * Method to get the array of response headers to be sent when the response is sent to the client. 569 | * 570 | * @return array 571 | * 572 | * @since 1.0.0 573 | */ 574 | public function getHeaders() 575 | { 576 | $return = []; 577 | 578 | foreach ($this->getResponse()->getHeaders() as $name => $values) { 579 | foreach ($values as $value) { 580 | $return[] = ['name' => $name, 'value' => $value]; 581 | } 582 | } 583 | 584 | return $return; 585 | } 586 | 587 | /** 588 | * Method to clear any set response headers. 589 | * 590 | * @return $this 591 | * 592 | * @since 1.0.0 593 | */ 594 | public function clearHeaders() 595 | { 596 | $response = $this->getResponse(); 597 | 598 | foreach ($response->getHeaders() as $name => $values) { 599 | $response = $response->withoutHeader($name); 600 | } 601 | 602 | $this->setResponse($response); 603 | 604 | return $this; 605 | } 606 | 607 | /** 608 | * Send the response headers. 609 | * 610 | * @return $this 611 | * 612 | * @since 1.0.0 613 | */ 614 | public function sendHeaders() 615 | { 616 | if (!$this->checkHeadersSent()) { 617 | foreach ($this->getHeaders() as $header) { 618 | if (\strtolower($header['name']) == 'status') { 619 | // 'status' headers indicate an HTTP status, and need to be handled slightly differently 620 | $status = $this->getHttpStatusValue($header['value']); 621 | 622 | $this->header($status, true, (int) $header['value']); 623 | } else { 624 | $this->header($header['name'] . ': ' . $header['value']); 625 | } 626 | } 627 | } 628 | 629 | return $this; 630 | } 631 | 632 | /** 633 | * Set body content. If body content already defined, this will replace it. 634 | * 635 | * @param string $content The content to set as the response body. 636 | * 637 | * @return $this 638 | * 639 | * @since 1.0.0 640 | */ 641 | public function setBody($content) 642 | { 643 | $stream = new Stream('php://memory', 'rw'); 644 | $stream->write((string) $content); 645 | $this->setResponse($this->getResponse()->withBody($stream)); 646 | 647 | return $this; 648 | } 649 | 650 | /** 651 | * Prepend content to the body content 652 | * 653 | * @param string $content The content to prepend to the response body. 654 | * 655 | * @return $this 656 | * 657 | * @since 1.0.0 658 | */ 659 | public function prependBody($content) 660 | { 661 | $currentBody = $this->getResponse()->getBody(); 662 | 663 | if (!$currentBody->isReadable()) { 664 | throw new UnableToWriteBody(); 665 | } 666 | 667 | $stream = new Stream('php://memory', 'rw'); 668 | $stream->write((string) $content . (string) $currentBody); 669 | $this->setResponse($this->getResponse()->withBody($stream)); 670 | 671 | return $this; 672 | } 673 | 674 | /** 675 | * Append content to the body content 676 | * 677 | * @param string $content The content to append to the response body. 678 | * 679 | * @return $this 680 | * 681 | * @since 1.0.0 682 | */ 683 | public function appendBody($content) 684 | { 685 | $currentStream = $this->getResponse()->getBody(); 686 | 687 | if ($currentStream->isWritable()) { 688 | $currentStream->write((string) $content); 689 | $this->setResponse($this->getResponse()->withBody($currentStream)); 690 | } elseif ($currentStream->isReadable()) { 691 | $stream = new Stream('php://memory', 'rw'); 692 | $stream->write((string) $currentStream . (string) $content); 693 | $this->setResponse($this->getResponse()->withBody($stream)); 694 | } else { 695 | throw new UnableToWriteBody(); 696 | } 697 | 698 | return $this; 699 | } 700 | 701 | /** 702 | * Return the body content 703 | * 704 | * @return string The response body as a string. 705 | * 706 | * @since 1.0.0 707 | */ 708 | public function getBody() 709 | { 710 | return (string) $this->getResponse()->getBody(); 711 | } 712 | 713 | /** 714 | * Get the PSR-7 Response Object. 715 | * 716 | * @return ResponseInterface 717 | * 718 | * @since 2.0.0 719 | */ 720 | public function getResponse(): ResponseInterface 721 | { 722 | return $this->response; 723 | } 724 | 725 | /** 726 | * Check if a given value can be successfully mapped to a valid http status value 727 | * 728 | * @param string|int $value The given status as int or string 729 | * 730 | * @return string 731 | * 732 | * @since 1.8.0 733 | */ 734 | protected function getHttpStatusValue($value) 735 | { 736 | $code = (int) $value; 737 | 738 | if (\array_key_exists($code, $this->responseMap)) { 739 | $value = $this->responseMap[$code]; 740 | } else { 741 | $value = 'HTTP/{version} ' . $code; 742 | } 743 | 744 | return \str_replace('{version}', $this->httpVersion, $value); 745 | } 746 | 747 | /** 748 | * Check if the value is a valid HTTP status code 749 | * 750 | * @param integer $code The potential status code 751 | * 752 | * @return boolean 753 | * 754 | * @since 1.8.1 755 | */ 756 | public function isValidHttpStatus($code) 757 | { 758 | return \array_key_exists($code, $this->responseMap); 759 | } 760 | 761 | /** 762 | * Method to check the current client connection status to ensure that it is alive. We are 763 | * wrapping this to isolate the \connection_status() function from our code base for testing reasons. 764 | * 765 | * @return boolean True if the connection is valid and normal. 766 | * 767 | * @codeCoverageIgnore 768 | * @see \connection_status() 769 | * @since 1.0.0 770 | */ 771 | protected function checkConnectionAlive() 772 | { 773 | return \connection_status() === CONNECTION_NORMAL; 774 | } 775 | 776 | /** 777 | * Method to check to see if headers have already been sent. 778 | * 779 | * @return boolean True if the headers have already been sent. 780 | * 781 | * @codeCoverageIgnore 782 | * @see \headers_sent() 783 | * @since 1.0.0 784 | */ 785 | protected function checkHeadersSent() 786 | { 787 | return \headers_sent(); 788 | } 789 | 790 | /** 791 | * Method to detect the requested URI from server environment variables. 792 | * 793 | * @return string The requested URI 794 | * 795 | * @since 1.0.0 796 | */ 797 | protected function detectRequestUri() 798 | { 799 | // First we need to detect the URI scheme. 800 | $scheme = $this->isSslConnection() ? 'https://' : 'http://'; 801 | 802 | /* 803 | * There are some differences in the way that Apache and IIS populate server environment variables. To 804 | * properly detect the requested URI we need to adjust our algorithm based on whether or not we are getting 805 | * information from Apache or IIS. 806 | */ 807 | 808 | $phpSelf = $this->input->server->getString('PHP_SELF', ''); 809 | $requestUri = $this->input->server->getString('REQUEST_URI', ''); 810 | 811 | $uri = $scheme . $this->input->server->getString('HTTP_HOST'); 812 | 813 | if (!empty($phpSelf) && !empty($requestUri)) { 814 | // If PHP_SELF and REQUEST_URI are both populated then we will assume "Apache Mode". 815 | // The URI is built from the HTTP_HOST and REQUEST_URI environment variables in an Apache environment. 816 | $uri .= $requestUri; 817 | } else { 818 | // If not in "Apache Mode" we will assume that we are in an IIS environment and proceed. 819 | // IIS uses the SCRIPT_NAME variable instead of a REQUEST_URI variable... thanks, MS 820 | $uri .= $this->input->server->getString('SCRIPT_NAME'); 821 | $queryHost = $this->input->server->getString('QUERY_STRING', ''); 822 | 823 | // If the QUERY_STRING variable exists append it to the URI string. 824 | if (!empty($queryHost)) { 825 | $uri .= '?' . $queryHost; 826 | } 827 | } 828 | 829 | // Extra cleanup to remove invalid chars in the URL to prevent injections through the Host header 830 | $uri = str_replace(["'", '"', '<', '>'], ['%27', '%22', '%3C', '%3E'], $uri); 831 | 832 | return \trim($uri); 833 | } 834 | 835 | /** 836 | * Method to send a header to the client. 837 | * 838 | * @param string $string The header string. 839 | * @param boolean $replace The optional replace parameter indicates whether the header should replace a 840 | * previous similar header, or add a second header of the same type. 841 | * @param integer $code Forces the HTTP response code to the specified value. Note that this parameter only 842 | * has an effect if the string is not empty. 843 | * 844 | * @return void 845 | * 846 | * @codeCoverageIgnore 847 | * @see \header() 848 | * @since 1.0.0 849 | */ 850 | protected function header($string, $replace = true, $code = null) 851 | { 852 | if ($code === null) { 853 | $code = 0; 854 | } 855 | 856 | \header(\str_replace(\chr(0), '', $string), $replace, $code); 857 | } 858 | 859 | /** 860 | * Set the PSR-7 Response Object. 861 | * 862 | * @param ResponseInterface $response The response object 863 | * 864 | * @return void 865 | * 866 | * @since 2.0.0 867 | */ 868 | public function setResponse(ResponseInterface $response): void 869 | { 870 | $this->response = $response; 871 | } 872 | 873 | /** 874 | * Checks if a state is a redirect state 875 | * 876 | * @param integer $state The HTTP status code. 877 | * 878 | * @return boolean 879 | * 880 | * @since 1.8.0 881 | */ 882 | protected function isRedirectState($state) 883 | { 884 | $state = (int) $state; 885 | 886 | return $state > 299 && $state < 400 && \array_key_exists($state, $this->responseMap); 887 | } 888 | 889 | /** 890 | * Determine if we are using a secure (SSL) connection. 891 | * 892 | * @return boolean True if using SSL, false if not. 893 | * 894 | * @since 1.0.0 895 | */ 896 | public function isSslConnection() 897 | { 898 | $serverSSLVar = $this->input->server->getString('HTTPS', ''); 899 | 900 | if (!empty($serverSSLVar) && \strtolower($serverSSLVar) !== 'off') { 901 | return true; 902 | } 903 | 904 | $serverForwarderProtoVar = $this->input->server->getString('HTTP_X_FORWARDED_PROTO', ''); 905 | 906 | return !empty($serverForwarderProtoVar) && \strtolower($serverForwarderProtoVar) === 'https'; 907 | } 908 | 909 | /** 910 | * Method to load the system URI strings for the application. 911 | * 912 | * @param string $requestUri An optional request URI to use instead of detecting one from the server environment 913 | * variables. 914 | * 915 | * @return void 916 | * 917 | * @since 1.0.0 918 | */ 919 | protected function loadSystemUris($requestUri = null) 920 | { 921 | // Set the request URI. 922 | if (!empty($requestUri)) { 923 | $this->set('uri.request', $requestUri); 924 | } else { 925 | $this->set('uri.request', $this->detectRequestUri()); 926 | } 927 | 928 | // Check to see if an explicit base URI has been set. 929 | $siteUri = \trim($this->get('site_uri', '')); 930 | 931 | if ($siteUri !== '') { 932 | $uri = new Uri($siteUri); 933 | $path = $uri->toString(['path']); 934 | } else { 935 | // No explicit base URI was set so we need to detect it. Start with the requested URI. 936 | $uri = new Uri($this->get('uri.request')); 937 | 938 | $requestUri = $this->input->server->getString('REQUEST_URI', ''); 939 | 940 | // If we are working from a CGI SAPI with the 'cgi.fix_pathinfo' directive disabled we use PHP_SELF. 941 | if (\strpos(PHP_SAPI, 'cgi') !== false && !\ini_get('cgi.fix_pathinfo') && !empty($requestUri)) { 942 | // We aren't expecting PATH_INFO within PHP_SELF so this should work. 943 | $path = \dirname($this->input->server->getString('PHP_SELF', '')); 944 | } else { 945 | // Pretty much everything else should be handled with SCRIPT_NAME. 946 | $path = \dirname($this->input->server->getString('SCRIPT_NAME', '')); 947 | } 948 | } 949 | 950 | // Get the host from the URI. 951 | $host = $uri->toString(['scheme', 'user', 'pass', 'host', 'port']); 952 | 953 | // Check if the path includes "index.php". 954 | if (\strpos($path, 'index.php') !== false) { 955 | // Remove the index.php portion of the path. 956 | $path = \substr_replace($path, '', \strpos($path, 'index.php'), 9); 957 | } 958 | 959 | $path = \rtrim($path, '/\\'); 960 | 961 | // Set the base URI both as just a path and as the full URI. 962 | $this->set('uri.base.full', $host . $path . '/'); 963 | $this->set('uri.base.host', $host); 964 | $this->set('uri.base.path', $path . '/'); 965 | 966 | // Set the extended (non-base) part of the request URI as the route. 967 | if (\stripos($this->get('uri.request'), $this->get('uri.base.full')) === 0) { 968 | $this->set( 969 | 'uri.route', 970 | \substr_replace($this->get('uri.request'), '', 0, \strlen($this->get('uri.base.full'))) 971 | ); 972 | } 973 | 974 | // Get an explicitly set media URI is present. 975 | $mediaURI = \trim($this->get('media_uri', '')); 976 | 977 | if ($mediaURI !== '') { 978 | if (\strpos($mediaURI, '://') !== false) { 979 | $this->set('uri.media.full', $mediaURI); 980 | } else { 981 | // Normalise slashes. 982 | $mediaURI = \trim($mediaURI, '/\\'); 983 | $mediaURI = !empty($mediaURI) ? '/' . $mediaURI . '/' : '/'; 984 | $this->set('uri.media.full', $this->get('uri.base.host') . $mediaURI); 985 | } 986 | $this->set('uri.media.path', $mediaURI); 987 | } else { 988 | // No explicit media URI was set, build it dynamically from the base uri. 989 | $this->set('uri.media.full', $this->get('uri.base.full') . 'media/'); 990 | $this->set('uri.media.path', $this->get('uri.base.path') . 'media/'); 991 | } 992 | } 993 | 994 | /** 995 | * Tests whether a string contains only 7bit ASCII bytes. 996 | * 997 | * You might use this to conditionally check whether a string 998 | * needs handling as UTF-8 or not, potentially offering performance 999 | * benefits by using the native PHP equivalent if it's just ASCII e.g.; 1000 | * 1001 | * @param string $str The string to test. 1002 | * 1003 | * @return boolean True if the string is all ASCII 1004 | * 1005 | * @since 1.4.0 1006 | */ 1007 | public static function isAscii($str) 1008 | { 1009 | // Search for any bytes which are outside the ASCII range... 1010 | return \preg_match('/[^\x00-\x7F]/', $str) !== 1; 1011 | } 1012 | } 1013 | -------------------------------------------------------------------------------- /src/ApplicationEvents.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License version 2 or later; see LICENSE 8 | */ 9 | 10 | namespace Joomla\Application; 11 | 12 | /** 13 | * Class defining the events available in the application. 14 | * 15 | * @since 2.0.0 16 | */ 17 | final class ApplicationEvents 18 | { 19 | /** 20 | * The ERROR event is an event triggered when a Throwable is uncaught. 21 | * 22 | * This event allows you to inspect the Throwable and implement additional error handling/reporting mechanisms. 23 | * 24 | * @var string 25 | * @since 2.0.0 26 | */ 27 | public const ERROR = 'application.error'; 28 | 29 | /** 30 | * The BEFORE_EXECUTE event is an event triggered before the application is executed. 31 | * 32 | * @var string 33 | * @since 2.0.0 34 | */ 35 | public const BEFORE_EXECUTE = 'application.before_execute'; 36 | 37 | /** 38 | * The AFTER_EXECUTE event is an event triggered after the application is executed. 39 | * 40 | * @var string 41 | * @since 2.0.0 42 | */ 43 | public const AFTER_EXECUTE = 'application.after_execute'; 44 | 45 | /** 46 | * The BEFORE_RESPOND event is an event triggered before the application response is sent. 47 | * 48 | * @var string 49 | * @since 2.0.0 50 | */ 51 | public const BEFORE_RESPOND = 'application.before_respond'; 52 | 53 | /** 54 | * The AFTER_RESPOND event is an event triggered after the application response is sent. 55 | * 56 | * @var string 57 | * @since 2.0.0 58 | */ 59 | public const AFTER_RESPOND = 'application.after_respond'; 60 | } 61 | -------------------------------------------------------------------------------- /src/ApplicationInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License version 2 or later; see LICENSE 8 | */ 9 | 10 | namespace Joomla\Application; 11 | 12 | /** 13 | * Joomla Framework Application Interface 14 | * 15 | * @since 2.0.0 16 | */ 17 | interface ApplicationInterface 18 | { 19 | /** 20 | * Method to close the application. 21 | * 22 | * @param integer $code The exit code (optional; default is 0). 23 | * 24 | * @return void 25 | * 26 | * @since 2.0.0 27 | */ 28 | public function close($code = 0); 29 | 30 | /** 31 | * Execute the application. 32 | * 33 | * @return void 34 | * 35 | * @since 2.0.0 36 | */ 37 | public function execute(); 38 | } 39 | -------------------------------------------------------------------------------- /src/ConfigurationAwareApplicationInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License version 2 or later; see LICENSE 8 | */ 9 | 10 | namespace Joomla\Application; 11 | 12 | use Joomla\Registry\Registry; 13 | 14 | /** 15 | * Application sub-interface defining an application class which is aware of its configuration 16 | * 17 | * @since 2.0.0 18 | */ 19 | interface ConfigurationAwareApplicationInterface extends ApplicationInterface 20 | { 21 | /** 22 | * Returns a property of the object or the default value if the property is not set. 23 | * 24 | * @param string $key The name of the property. 25 | * @param mixed $default The default value (optional) if none is set. 26 | * 27 | * @return mixed The value of the configuration. 28 | * 29 | * @since 2.0.0 30 | */ 31 | public function get($key, $default = null); 32 | 33 | /** 34 | * Modifies a property of the object, creating it if it does not already exist. 35 | * 36 | * @param string $key The name of the property. 37 | * @param mixed $value The value of the property to set (optional). 38 | * 39 | * @return mixed Previous value of the property 40 | * 41 | * @since 2.0.0 42 | */ 43 | public function set($key, $value = null); 44 | 45 | /** 46 | * Sets the configuration for the application. 47 | * 48 | * @param Registry $config A registry object holding the configuration. 49 | * 50 | * @return $this 51 | * 52 | * @since 2.0.0 53 | */ 54 | public function setConfiguration(Registry $config); 55 | } 56 | -------------------------------------------------------------------------------- /src/Controller/ContainerControllerResolver.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License version 2 or later; see LICENSE 8 | */ 9 | 10 | namespace Joomla\Application\Controller; 11 | 12 | use Psr\Container\ContainerInterface; 13 | 14 | /** 15 | * Controller resolver which supports creating controllers from a PSR-11 compatible container 16 | * 17 | * Controllers must be registered in the container using their FQCN as a service key 18 | * 19 | * @since 2.0.0 20 | */ 21 | class ContainerControllerResolver extends ControllerResolver 22 | { 23 | /** 24 | * The container to search for controllers in 25 | * 26 | * @var ContainerInterface 27 | * @since 2.0.0 28 | */ 29 | private $container; 30 | 31 | /** 32 | * Constructor 33 | * 34 | * @param ContainerInterface $container The container to search for controllers in 35 | * 36 | * @since 2.0.0 37 | */ 38 | public function __construct(ContainerInterface $container) 39 | { 40 | $this->container = $container; 41 | } 42 | 43 | /** 44 | * Instantiate a controller class 45 | * 46 | * @param string $class The class to instantiate 47 | * 48 | * @return object Controller class instance 49 | * 50 | * @since 2.0.0 51 | */ 52 | protected function instantiateController(string $class): object 53 | { 54 | if ($this->container->has($class)) { 55 | return $this->container->get($class); 56 | } 57 | 58 | return parent::instantiateController($class); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Controller/ControllerResolver.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License version 2 or later; see LICENSE 8 | */ 9 | 10 | namespace Joomla\Application\Controller; 11 | 12 | use Joomla\Controller\ControllerInterface; 13 | use Joomla\Router\ResolvedRoute; 14 | 15 | /** 16 | * Resolves a controller for the given route. 17 | * 18 | * @since 2.0.0 19 | */ 20 | class ControllerResolver implements ControllerResolverInterface 21 | { 22 | /** 23 | * Resolve the controller for a route 24 | * 25 | * @param ResolvedRoute $route The route to resolve the controller for 26 | * 27 | * @return callable 28 | * 29 | * @throws \InvalidArgumentException 30 | * @since 2.0.0 31 | */ 32 | public function resolve(ResolvedRoute $route): callable 33 | { 34 | $controller = $route->getController(); 35 | 36 | // Try to resolve a callable defined as an array 37 | if (\is_array($controller)) { 38 | if (isset($controller[0]) && \is_string($controller[0]) && isset($controller[1])) { 39 | if (!\class_exists($controller[0])) { 40 | throw new \InvalidArgumentException( 41 | \sprintf('Cannot resolve controller for URI `%s`', $route->getUri()) 42 | ); 43 | } 44 | 45 | try { 46 | $controller[0] = $this->instantiateController($controller[0]); 47 | } catch (\ArgumentCountError $error) { 48 | throw new \InvalidArgumentException( 49 | \sprintf( 50 | 'Controller `%s` has required constructor arguments, cannot instantiate the class', 51 | $controller[0] 52 | ), 53 | 0, 54 | $error 55 | ); 56 | } 57 | } 58 | 59 | if (!\is_callable($controller)) { 60 | throw new \InvalidArgumentException( 61 | \sprintf('Cannot resolve controller for URI `%s`', $route->getUri()) 62 | ); 63 | } 64 | 65 | return $controller; 66 | } 67 | 68 | // Try to resolve an invocable object 69 | if (\is_object($controller)) { 70 | if (!\is_callable($controller)) { 71 | throw new \InvalidArgumentException( 72 | \sprintf('Cannot resolve controller for URI `%s`', $route->getUri()) 73 | ); 74 | } 75 | 76 | return $controller; 77 | } 78 | 79 | // Try to resolve a known function 80 | if (\function_exists($controller)) { 81 | return $controller; 82 | } 83 | 84 | // Try to resolve a class name if it implements our ControllerInterface 85 | if (\is_string($controller) && \interface_exists(ControllerInterface::class)) { 86 | if (!\class_exists($controller)) { 87 | throw new \InvalidArgumentException( 88 | \sprintf('Cannot resolve controller for URI `%s`', $route->getUri()) 89 | ); 90 | } 91 | 92 | try { 93 | return [$this->instantiateController($controller), 'execute']; 94 | } catch (\ArgumentCountError $error) { 95 | throw new \InvalidArgumentException( 96 | \sprintf( 97 | 'Controller `%s` has required constructor arguments, cannot instantiate the class', 98 | $controller 99 | ), 100 | 0, 101 | $error 102 | ); 103 | } 104 | } 105 | 106 | // Unsupported resolution 107 | throw new \InvalidArgumentException(\sprintf('Cannot resolve controller for URI `%s`', $route->getUri())); 108 | } 109 | 110 | /** 111 | * Instantiate a controller class 112 | * 113 | * @param string $class The class to instantiate 114 | * 115 | * @return object Controller class instance 116 | * 117 | * @since 2.0.0 118 | */ 119 | protected function instantiateController(string $class): object 120 | { 121 | return new $class(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Controller/ControllerResolverInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License version 2 or later; see LICENSE 8 | */ 9 | 10 | namespace Joomla\Application\Controller; 11 | 12 | use Joomla\Router\ResolvedRoute; 13 | 14 | /** 15 | * Interface defining a controller resolver. 16 | * 17 | * @since 2.0.0 18 | */ 19 | interface ControllerResolverInterface 20 | { 21 | /** 22 | * Resolve the controller for a route 23 | * 24 | * @param ResolvedRoute $route The route to resolve the controller for 25 | * 26 | * @return callable 27 | * 28 | * @since 2.0.0 29 | * @throws \InvalidArgumentException 30 | */ 31 | public function resolve(ResolvedRoute $route): callable; 32 | } 33 | -------------------------------------------------------------------------------- /src/Event/ApplicationErrorEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License version 2 or later; see LICENSE 8 | */ 9 | 10 | namespace Joomla\Application\Event; 11 | 12 | use Joomla\Application\AbstractApplication; 13 | use Joomla\Application\ApplicationEvents; 14 | 15 | /** 16 | * Event class thrown when an application error occurs. 17 | * 18 | * @since 2.0.0 19 | */ 20 | class ApplicationErrorEvent extends ApplicationEvent 21 | { 22 | /** 23 | * The Throwable object with the error data. 24 | * 25 | * @var \Throwable 26 | * @since 2.0.0 27 | */ 28 | private $error; 29 | 30 | /** 31 | * Event constructor. 32 | * 33 | * @param \Throwable $error The Throwable object with the error data. 34 | * @param AbstractApplication $application The active application. 35 | * 36 | * @since 2.0.0 37 | */ 38 | public function __construct(\Throwable $error, AbstractApplication $application) 39 | { 40 | parent::__construct(ApplicationEvents::ERROR, $application); 41 | 42 | $this->error = $error; 43 | } 44 | 45 | /** 46 | * Get the error object. 47 | * 48 | * @return \Throwable 49 | * 50 | * @since 2.0.0 51 | */ 52 | public function getError(): \Throwable 53 | { 54 | return $this->error; 55 | } 56 | 57 | /** 58 | * Set the error object. 59 | * 60 | * @param \Throwable $error The error object to set to the event. 61 | * 62 | * @return void 63 | * 64 | * @since 2.0.0 65 | */ 66 | public function setError(\Throwable $error): void 67 | { 68 | $this->error = $error; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Event/ApplicationEvent.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License version 2 or later; see LICENSE 8 | */ 9 | 10 | namespace Joomla\Application\Event; 11 | 12 | use Joomla\Application\AbstractApplication; 13 | use Joomla\Event\Event; 14 | 15 | /** 16 | * Base event class for application events. 17 | * 18 | * @since 2.0.0 19 | */ 20 | class ApplicationEvent extends Event 21 | { 22 | /** 23 | * The active application. 24 | * 25 | * @var AbstractApplication 26 | * @since 2.0.0 27 | */ 28 | private $application; 29 | 30 | /** 31 | * Event constructor. 32 | * 33 | * @param string $name The event name. 34 | * @param AbstractApplication $application The active application. 35 | * 36 | * @since 2.0.0 37 | */ 38 | public function __construct(string $name, AbstractApplication $application) 39 | { 40 | parent::__construct($name); 41 | 42 | $this->application = $application; 43 | } 44 | 45 | /** 46 | * Get the active application. 47 | * 48 | * @return AbstractApplication 49 | * 50 | * @since 2.0.0 51 | */ 52 | public function getApplication(): AbstractApplication 53 | { 54 | return $this->application; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Exception/UnableToWriteBody.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License version 2 or later; see LICENSE 8 | */ 9 | 10 | namespace Joomla\Application\Exception; 11 | 12 | /** 13 | * Exception thrown when the application can't write to the response body 14 | * 15 | * @since 2.0.0 16 | */ 17 | class UnableToWriteBody extends \DomainException 18 | { 19 | } 20 | -------------------------------------------------------------------------------- /src/SessionAwareWebApplicationInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License version 2 or later; see LICENSE 8 | */ 9 | 10 | namespace Joomla\Application; 11 | 12 | use Joomla\Session\SessionInterface; 13 | 14 | /** 15 | * Application sub-interface defining a web application class which supports sessions 16 | * 17 | * @since 2.0.0 18 | */ 19 | interface SessionAwareWebApplicationInterface extends WebApplicationInterface 20 | { 21 | /** 22 | * Method to get the application session object. 23 | * 24 | * @return SessionInterface The session object 25 | * 26 | * @since 2.0.0 27 | */ 28 | public function getSession(); 29 | 30 | /** 31 | * Sets the session for the application to use, if required. 32 | * 33 | * @param SessionInterface $session A session object. 34 | * 35 | * @return $this 36 | * 37 | * @since 2.0.0 38 | */ 39 | public function setSession(SessionInterface $session); 40 | 41 | /** 42 | * Checks for a form token in the request. 43 | * 44 | * @param string $method The request method in which to look for the token key. 45 | * 46 | * @return boolean 47 | * 48 | * @since 2.0.0 49 | */ 50 | public function checkToken($method = 'post'); 51 | 52 | /** 53 | * Method to determine a hash for anti-spoofing variable names 54 | * 55 | * @param boolean $forceNew If true, force a new token to be created 56 | * 57 | * @return string Hashed var name 58 | * 59 | * @since 2.0.0 60 | */ 61 | public function getFormToken($forceNew = false); 62 | } 63 | -------------------------------------------------------------------------------- /src/SessionAwareWebApplicationTrait.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License version 2 or later; see LICENSE 8 | */ 9 | 10 | namespace Joomla\Application; 11 | 12 | use Joomla\Input\Input; 13 | use Joomla\Session\SessionInterface; 14 | 15 | /** 16 | * Trait which helps implementing `Joomla\Application\SessionAwareWebApplicationInterface` in a web application class. 17 | * 18 | * @since 2.0.0 19 | */ 20 | trait SessionAwareWebApplicationTrait 21 | { 22 | /** 23 | * The application session object. 24 | * 25 | * @var SessionInterface 26 | * @since 2.0.0 27 | */ 28 | protected $session; 29 | 30 | /** 31 | * Method to get the application input object. 32 | * 33 | * @return Input 34 | * 35 | * @since 2.0.0 36 | */ 37 | abstract public function getInput(): Input; 38 | 39 | /** 40 | * Method to get the application session object. 41 | * 42 | * @return SessionInterface The session object 43 | * 44 | * @since 2.0.0 45 | */ 46 | public function getSession() 47 | { 48 | if ($this->session === null) { 49 | throw new \RuntimeException(\sprintf('A %s object has not been set.', SessionInterface::class)); 50 | } 51 | 52 | return $this->session; 53 | } 54 | 55 | /** 56 | * Sets the session for the application to use, if required. 57 | * 58 | * @param SessionInterface $session A session object. 59 | * 60 | * @return $this 61 | * 62 | * @since 2.0.0 63 | */ 64 | public function setSession(SessionInterface $session) 65 | { 66 | $this->session = $session; 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * Checks for a form token in the request. 73 | * 74 | * @param string $method The request method in which to look for the token key. 75 | * 76 | * @return boolean 77 | * 78 | * @since 2.0.0 79 | */ 80 | public function checkToken($method = 'post') 81 | { 82 | $token = $this->getFormToken(); 83 | 84 | // Support a token sent via the X-CSRF-Token header, then fall back to a token in the request 85 | $requestToken = $this->getInput()->server->get( 86 | 'HTTP_X_CSRF_TOKEN', 87 | $this->getInput()->$method->get($token, '', 'alnum'), 88 | 'alnum' 89 | ); 90 | 91 | if (!$requestToken) { 92 | return false; 93 | } 94 | 95 | return $this->getSession()->hasToken($token); 96 | } 97 | 98 | /** 99 | * Method to determine a hash for anti-spoofing variable names 100 | * 101 | * @param boolean $forceNew If true, force a new token to be created 102 | * 103 | * @return string Hashed var name 104 | * 105 | * @since 2.0.0 106 | */ 107 | public function getFormToken($forceNew = false) 108 | { 109 | return $this->getSession()->getToken($forceNew); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Web/WebClient.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License version 2 or later; see LICENSE 8 | */ 9 | 10 | namespace Joomla\Application\Web; 11 | 12 | /** 13 | * Class to model a Web Client. 14 | * 15 | * @property-read integer $platform The detected platform on which the web client runs. 16 | * @property-read boolean $mobile True if the web client is a mobile device. 17 | * @property-read integer $engine The detected rendering engine used by the web client. 18 | * @property-read integer $browser The detected browser used by the web client. 19 | * @property-read string $browserVersion The detected browser version used by the web client. 20 | * @property-read array $languages The priority order detected accepted languages for the client. 21 | * @property-read array $encodings The priority order detected accepted encodings for the client. 22 | * @property-read string $userAgent The web client's user agent string. 23 | * @property-read string $acceptEncoding The web client's accepted encoding string. 24 | * @property-read string $acceptLanguage The web client's accepted languages string. 25 | * @property-read array $detection An array of flags determining whether a detection routine has been run. 26 | * @property-read boolean $robot True if the web client is a robot 27 | * @property-read array $headers An array of all headers sent by client 28 | * 29 | * @since 1.0.0 30 | */ 31 | class WebClient 32 | { 33 | public const WINDOWS = 1; 34 | public const WINDOWS_PHONE = 2; 35 | public const WINDOWS_CE = 3; 36 | public const IPHONE = 4; 37 | public const IPAD = 5; 38 | public const IPOD = 6; 39 | public const MAC = 7; 40 | public const BLACKBERRY = 8; 41 | public const ANDROID = 9; 42 | public const LINUX = 10; 43 | public const TRIDENT = 11; 44 | public const WEBKIT = 12; 45 | public const GECKO = 13; 46 | public const PRESTO = 14; 47 | public const KHTML = 15; 48 | public const AMAYA = 16; 49 | public const IE = 17; 50 | public const FIREFOX = 18; 51 | public const CHROME = 19; 52 | public const SAFARI = 20; 53 | public const OPERA = 21; 54 | public const ANDROIDTABLET = 22; 55 | public const EDGE = 23; 56 | public const BLINK = 24; 57 | public const EDG = 25; 58 | 59 | /** 60 | * The detected platform on which the web client runs. 61 | * 62 | * @var integer 63 | * @since 1.0.0 64 | */ 65 | protected $platform; 66 | 67 | /** 68 | * True if the web client is a mobile device. 69 | * 70 | * @var boolean 71 | * @since 1.0.0 72 | */ 73 | protected $mobile = false; 74 | 75 | /** 76 | * The detected rendering engine used by the web client. 77 | * 78 | * @var integer 79 | * @since 1.0.0 80 | */ 81 | protected $engine; 82 | 83 | /** 84 | * The detected browser used by the web client. 85 | * 86 | * @var integer 87 | * @since 1.0.0 88 | */ 89 | protected $browser; 90 | 91 | /** 92 | * The detected browser version used by the web client. 93 | * 94 | * @var string 95 | * @since 1.0.0 96 | */ 97 | protected $browserVersion; 98 | 99 | /** 100 | * The priority order detected accepted languages for the client. 101 | * 102 | * @var array 103 | * @since 1.0.0 104 | */ 105 | protected $languages = []; 106 | 107 | /** 108 | * The priority order detected accepted encodings for the client. 109 | * 110 | * @var array 111 | * @since 1.0.0 112 | */ 113 | protected $encodings = []; 114 | 115 | /** 116 | * The web client's user agent string. 117 | * 118 | * @var string 119 | * @since 1.0.0 120 | */ 121 | protected $userAgent; 122 | 123 | /** 124 | * The web client's accepted encoding string. 125 | * 126 | * @var string 127 | * @since 1.0.0 128 | */ 129 | protected $acceptEncoding; 130 | 131 | /** 132 | * The web client's accepted languages string. 133 | * 134 | * @var string 135 | * @since 1.0.0 136 | */ 137 | protected $acceptLanguage; 138 | 139 | /** 140 | * True if the web client is a robot. 141 | * 142 | * @var boolean 143 | * @since 1.0.0 144 | */ 145 | protected $robot = false; 146 | 147 | /** 148 | * An array of flags determining whether or not a detection routine has been run. 149 | * 150 | * @var array 151 | * @since 1.0.0 152 | */ 153 | protected $detection = []; 154 | 155 | /** 156 | * An array of headers sent by client. 157 | * 158 | * @var array 159 | * @since 1.3.0 160 | */ 161 | protected $headers; 162 | 163 | /** 164 | * Class constructor. 165 | * 166 | * @param string $userAgent The optional user-agent string to parse. 167 | * @param string $acceptEncoding The optional client accept encoding string to parse. 168 | * @param string $acceptLanguage The optional client accept language string to parse. 169 | * 170 | * @since 1.0.0 171 | */ 172 | public function __construct($userAgent = null, $acceptEncoding = null, $acceptLanguage = null) 173 | { 174 | // If no explicit user agent string was given attempt to use the implicit one from server environment. 175 | if (empty($userAgent) && isset($_SERVER['HTTP_USER_AGENT'])) { 176 | $this->userAgent = $_SERVER['HTTP_USER_AGENT']; 177 | } else { 178 | $this->userAgent = $userAgent; 179 | } 180 | 181 | // If no explicit acceptable encoding string was given attempt to use the implicit one from server environment. 182 | if (empty($acceptEncoding) && isset($_SERVER['HTTP_ACCEPT_ENCODING'])) { 183 | $this->acceptEncoding = $_SERVER['HTTP_ACCEPT_ENCODING']; 184 | } else { 185 | $this->acceptEncoding = $acceptEncoding; 186 | } 187 | 188 | // If no explicit acceptable languages string was given attempt to use the implicit one from server environment. 189 | if (empty($acceptLanguage) && isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) { 190 | $this->acceptLanguage = $_SERVER['HTTP_ACCEPT_LANGUAGE']; 191 | } else { 192 | $this->acceptLanguage = $acceptLanguage; 193 | } 194 | } 195 | 196 | /** 197 | * Magic method to get an object property's value by name. 198 | * 199 | * @param string $name Name of the property for which to return a value. 200 | * 201 | * @return mixed The requested value if it exists. 202 | * 203 | * @since 1.0.0 204 | */ 205 | public function __get($name) 206 | { 207 | switch ($name) { 208 | case 'mobile': 209 | case 'platform': 210 | if (empty($this->detection['platform'])) { 211 | $this->detectPlatform($this->userAgent); 212 | } 213 | 214 | break; 215 | 216 | case 'engine': 217 | if (empty($this->detection['engine'])) { 218 | $this->detectEngine($this->userAgent); 219 | } 220 | 221 | break; 222 | 223 | case 'browser': 224 | case 'browserVersion': 225 | if (empty($this->detection['browser'])) { 226 | $this->detectBrowser($this->userAgent); 227 | } 228 | 229 | break; 230 | 231 | case 'languages': 232 | if (empty($this->detection['acceptLanguage'])) { 233 | $this->detectLanguage($this->acceptLanguage); 234 | } 235 | 236 | break; 237 | 238 | case 'encodings': 239 | if (empty($this->detection['acceptEncoding'])) { 240 | $this->detectEncoding($this->acceptEncoding); 241 | } 242 | 243 | break; 244 | 245 | case 'robot': 246 | if (empty($this->detection['robot'])) { 247 | $this->detectRobot($this->userAgent); 248 | } 249 | 250 | break; 251 | 252 | case 'headers': 253 | if (empty($this->detection['headers'])) { 254 | $this->detectHeaders(); 255 | } 256 | 257 | break; 258 | } 259 | 260 | // Return the property if it exists. 261 | if (\property_exists($this, $name)) { 262 | return $this->$name; 263 | } 264 | 265 | $trace = \debug_backtrace(); 266 | \trigger_error( 267 | 'Undefined property via \__get(): ' . $name . ' in ' . $trace[0]['file'] . ' on line ' . $trace[0]['line'], 268 | E_USER_NOTICE 269 | ); 270 | } 271 | 272 | /** 273 | * Detects the client browser and version in a user agent string. 274 | * 275 | * @param string $userAgent The user-agent string to parse. 276 | * 277 | * @return void 278 | * 279 | * @since 1.0.0 280 | */ 281 | protected function detectBrowser($userAgent) 282 | { 283 | // Mark this detection routine as run. 284 | $this->detection['browser'] = true; 285 | 286 | if (empty($userAgent)) { 287 | return; 288 | } 289 | 290 | // Check for Google's Private Prefetch Proxy 291 | if ($userAgent === 'Chrome Privacy Preserving Prefetch Proxy') { 292 | // Private Prefetch Proxy does not provide any further details like e.g. version 293 | $this->browser = self::CHROME; 294 | 295 | return; 296 | } 297 | 298 | $patternBrowser = ''; 299 | 300 | // Attempt to detect the browser type. Obviously we are only worried about major browsers. 301 | if ((\stripos($userAgent, 'MSIE') !== false) && (\stripos($userAgent, 'Opera') === false)) { 302 | $this->browser = self::IE; 303 | $patternBrowser = 'MSIE'; 304 | } elseif (\stripos($userAgent, 'Trident') !== false) { 305 | $this->browser = self::IE; 306 | $patternBrowser = ' rv'; 307 | } elseif (\stripos($userAgent, 'Edge') !== false) { 308 | $this->browser = self::EDGE; 309 | $patternBrowser = 'Edge'; 310 | } elseif (\stripos($userAgent, 'Edg') !== false) { 311 | $this->browser = self::EDG; 312 | $patternBrowser = 'Edg'; 313 | } elseif ((\stripos($userAgent, 'Firefox') !== false) && (\stripos($userAgent, 'like Firefox') === false)) { 314 | $this->browser = self::FIREFOX; 315 | $patternBrowser = 'Firefox'; 316 | } elseif (\stripos($userAgent, 'OPR') !== false) { 317 | $this->browser = self::OPERA; 318 | $patternBrowser = 'OPR'; 319 | } elseif (\stripos($userAgent, 'Chrome') !== false) { 320 | $this->browser = self::CHROME; 321 | $patternBrowser = 'Chrome'; 322 | } elseif (\stripos($userAgent, 'Safari') !== false) { 323 | $this->browser = self::SAFARI; 324 | $patternBrowser = 'Safari'; 325 | } elseif (\stripos($userAgent, 'Opera') !== false) { 326 | $this->browser = self::OPERA; 327 | $patternBrowser = 'Opera'; 328 | } 329 | 330 | // If we detected a known browser let's attempt to determine the version. 331 | if ($this->browser) { 332 | // Build the REGEX pattern to match the browser version string within the user agent string. 333 | $pattern = '#(?Version|' . $patternBrowser . ')[/ :]+(?[0-9.|a-zA-Z.]*)#'; 334 | 335 | // Attempt to find version strings in the user agent string. 336 | $matches = []; 337 | 338 | if (\preg_match_all($pattern, $userAgent, $matches)) { 339 | // Do we have both a Version and browser match? 340 | if (\count($matches['browser']) == 2) { 341 | // See whether Version or browser came first, and use the number accordingly. 342 | if (\strripos($userAgent, 'Version') < \strripos($userAgent, $patternBrowser)) { 343 | $this->browserVersion = $matches['version'][0]; 344 | } else { 345 | $this->browserVersion = $matches['version'][1]; 346 | } 347 | } elseif (\count($matches['browser']) > 2) { 348 | $key = \array_search('Version', $matches['browser']); 349 | 350 | if ($key) { 351 | $this->browserVersion = $matches['version'][$key]; 352 | } 353 | } else { 354 | // We only have a Version or a browser so use what we have. 355 | $this->browserVersion = $matches['version'][0]; 356 | } 357 | } 358 | } 359 | } 360 | 361 | /** 362 | * Method to detect the accepted response encoding by the client. 363 | * 364 | * @param string $acceptEncoding The client accept encoding string to parse. 365 | * 366 | * @return void 367 | * 368 | * @since 1.0.0 369 | */ 370 | protected function detectEncoding($acceptEncoding) 371 | { 372 | // Parse the accepted encodings. 373 | $this->encodings = \array_map('trim', (array) \explode(',', (string) $acceptEncoding)); 374 | 375 | // Mark this detection routine as run. 376 | $this->detection['acceptEncoding'] = true; 377 | } 378 | 379 | /** 380 | * Detects the client rendering engine in a user agent string. 381 | * 382 | * @param string $userAgent The user-agent string to parse. 383 | * 384 | * @return void 385 | * 386 | * @since 1.0.0 387 | */ 388 | protected function detectEngine($userAgent) 389 | { 390 | // Mark this detection routine as run. 391 | $this->detection['engine'] = true; 392 | 393 | if (empty($userAgent)) { 394 | return; 395 | } 396 | 397 | if (\stripos($userAgent, 'MSIE') !== false || \stripos($userAgent, 'Trident') !== false) { 398 | // Attempt to detect the client engine -- starting with the most popular ... for now. 399 | $this->engine = self::TRIDENT; 400 | } elseif (\stripos($userAgent, 'Edge') !== false || \stripos($userAgent, 'EdgeHTML') !== false) { 401 | $this->engine = self::EDGE; 402 | } elseif (\stripos($userAgent, 'Edg') !== false) { 403 | $this->engine = self::BLINK; 404 | } elseif (\stripos($userAgent, 'Chrome') !== false) { 405 | $this->engine = self::BLINK; 406 | 407 | $result = \explode('/', \stristr($userAgent, 'Chrome')); 408 | 409 | if (isset($result[1])) { 410 | $version = \explode(' ', $result[1]); 411 | 412 | if (version_compare($version[0], '28.0', 'lt')) { 413 | $this->engine = self::WEBKIT; 414 | } 415 | } 416 | } elseif (\stripos($userAgent, 'AppleWebKit') !== false || \stripos($userAgent, 'blackberry') !== false) { 417 | $this->engine = self::WEBKIT; 418 | 419 | if (\stripos($userAgent, 'AppleWebKit') !== false) { 420 | $result = \explode('/', \stristr($userAgent, 'AppleWebKit')); 421 | 422 | if (isset($result[1])) { 423 | $version = \explode(' ', $result[1]); 424 | 425 | if ($version[0] === '537.36') { 426 | // AppleWebKit/537.36 is Blink engine specific, exception is Blink emulated IEMobile, Trident or Edge 427 | $this->engine = self::BLINK; 428 | } 429 | } 430 | } 431 | } elseif (\stripos($userAgent, 'Gecko') !== false && \stripos($userAgent, 'like Gecko') === false) { 432 | // We have to check for like Gecko because some other browsers spoof Gecko. 433 | $this->engine = self::GECKO; 434 | } elseif (\stripos($userAgent, 'Opera') !== false || \stripos($userAgent, 'Presto') !== false) { 435 | $version = false; 436 | 437 | if (\preg_match('/Opera[\/| ]?([0-9.]+)/u', $userAgent, $match)) { 438 | $version = (float) ($match[1]); 439 | } 440 | 441 | if (\preg_match('/Version\/([0-9.]+)/u', $userAgent, $match)) { 442 | if ((float) ($match[1]) >= 10) { 443 | $version = (float) ($match[1]); 444 | } 445 | } 446 | 447 | if ($version !== false && $version >= 15) { 448 | $this->engine = self::BLINK; 449 | } else { 450 | $this->engine = self::PRESTO; 451 | } 452 | } elseif (\stripos($userAgent, 'KHTML') !== false) { 453 | // *sigh* 454 | $this->engine = self::KHTML; 455 | } elseif (\stripos($userAgent, 'Amaya') !== false) { 456 | // Lesser known engine but it finishes off the major list from Wikipedia :-) 457 | $this->engine = self::AMAYA; 458 | } 459 | } 460 | 461 | /** 462 | * Method to detect the accepted languages by the client. 463 | * 464 | * @param mixed $acceptLanguage The client accept language string to parse. 465 | * 466 | * @return void 467 | * 468 | * @since 1.0.0 469 | */ 470 | protected function detectLanguage($acceptLanguage) 471 | { 472 | // Parse the accepted encodings. 473 | $this->languages = \array_map('trim', (array) \explode(',', $acceptLanguage)); 474 | 475 | // Mark this detection routine as run. 476 | $this->detection['acceptLanguage'] = true; 477 | } 478 | 479 | /** 480 | * Detects the client platform in a user agent string. 481 | * 482 | * @param string $userAgent The user-agent string to parse. 483 | * 484 | * @return void 485 | * 486 | * @since 1.0.0 487 | */ 488 | protected function detectPlatform($userAgent) 489 | { 490 | // Mark this detection routine as run. 491 | $this->detection['platform'] = true; 492 | 493 | if (empty($userAgent)) { 494 | return; 495 | } 496 | 497 | // Attempt to detect the client platform. 498 | if (\stripos($userAgent, 'Windows') !== false) { 499 | $this->platform = self::WINDOWS; 500 | 501 | // Let's look at the specific mobile options in the Windows space. 502 | if (\stripos($userAgent, 'Windows Phone') !== false) { 503 | $this->mobile = true; 504 | $this->platform = self::WINDOWS_PHONE; 505 | } elseif (\stripos($userAgent, 'Windows CE') !== false) { 506 | $this->mobile = true; 507 | $this->platform = self::WINDOWS_CE; 508 | } 509 | } elseif (\stripos($userAgent, 'iPhone') !== false) { 510 | // Interestingly 'iPhone' is present in all iOS devices so far including iPad and iPods. 511 | $this->mobile = true; 512 | $this->platform = self::IPHONE; 513 | 514 | // Let's look at the specific mobile options in the iOS space. 515 | if (\stripos($userAgent, 'iPad') !== false) { 516 | $this->platform = self::IPAD; 517 | } elseif (\stripos($userAgent, 'iPod') !== false) { 518 | $this->platform = self::IPOD; 519 | } 520 | } elseif (\stripos($userAgent, 'iPad') !== false) { 521 | // In case where iPhone is not mentioed in iPad user agent string 522 | $this->mobile = true; 523 | $this->platform = self::IPAD; 524 | } elseif (\stripos($userAgent, 'iPod') !== false) { 525 | // In case where iPhone is not mentioed in iPod user agent string 526 | $this->mobile = true; 527 | $this->platform = self::IPOD; 528 | } elseif (\preg_match('/macintosh|mac os x/i', $userAgent)) { 529 | // This has to come after the iPhone check because mac strings are also present in iOS devices. 530 | $this->platform = self::MAC; 531 | } elseif (\stripos($userAgent, 'Blackberry') !== false) { 532 | $this->mobile = true; 533 | $this->platform = self::BLACKBERRY; 534 | } elseif (\stripos($userAgent, 'Android') !== false) { 535 | $this->mobile = true; 536 | $this->platform = self::ANDROID; 537 | 538 | /* 539 | * Attempt to distinguish between Android phones and tablets 540 | * There is no totally foolproof method but certain rules almost always hold 541 | * Android 3.x is only used for tablets 542 | * Some devices and browsers encourage users to change their UA string to include Tablet. 543 | * Google encourages manufacturers to exclude the string Mobile from tablet device UA strings. 544 | * In some modes Kindle Android devices include the string Mobile but they include the string Silk. 545 | */ 546 | if ( 547 | \stripos($userAgent, 'Android 3') !== false || \stripos($userAgent, 'Tablet') !== false 548 | || \stripos($userAgent, 'Mobile') === false || \stripos($userAgent, 'Silk') !== false 549 | ) { 550 | $this->platform = self::ANDROIDTABLET; 551 | } 552 | } elseif (\stripos($userAgent, 'Linux') !== false) { 553 | $this->platform = self::LINUX; 554 | } 555 | } 556 | 557 | /** 558 | * Determines if the browser is a robot or not. 559 | * 560 | * @param string $userAgent The user-agent string to parse. 561 | * 562 | * @return void 563 | * 564 | * @since 1.0.0 565 | */ 566 | protected function detectRobot($userAgent) 567 | { 568 | $this->detection['robot'] = true; 569 | 570 | if (empty($userAgent)) { 571 | return; 572 | } 573 | 574 | $this->robot = (bool) \preg_match('/http|bot|robot|spider|crawler|curl|^$/i', $userAgent); 575 | } 576 | 577 | /** 578 | * Fills internal array of headers 579 | * 580 | * @return void 581 | * 582 | * @since 1.3.0 583 | */ 584 | protected function detectHeaders() 585 | { 586 | if (\function_exists('getallheaders')) { 587 | // If php is working under Apache, there is a special function 588 | $this->headers = \getallheaders(); 589 | } else { 590 | // Else we fill headers from $_SERVER variable 591 | $this->headers = []; 592 | 593 | foreach ($_SERVER as $name => $value) { 594 | if (\substr($name, 0, 5) == 'HTTP_') { 595 | $this->headers[\str_replace(' ', '-', \ucwords(\strtolower(\str_replace('_', ' ', \substr($name, 5)))))] = $value; 596 | } 597 | } 598 | } 599 | 600 | // Mark this detection routine as run. 601 | $this->detection['headers'] = true; 602 | } 603 | } 604 | -------------------------------------------------------------------------------- /src/WebApplication.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License version 2 or later; see LICENSE 8 | */ 9 | 10 | namespace Joomla\Application; 11 | 12 | use Joomla\Application\Controller\ControllerResolverInterface; 13 | use Joomla\Application\Web\WebClient; 14 | use Joomla\Input\Input; 15 | use Joomla\Registry\Registry; 16 | use Joomla\Router\RouterInterface; 17 | use Psr\Http\Message\ResponseInterface; 18 | 19 | /** 20 | * A basic web application class for handing HTTP requests. 21 | * 22 | * @since 2.0.0 23 | */ 24 | class WebApplication extends AbstractWebApplication implements SessionAwareWebApplicationInterface 25 | { 26 | use SessionAwareWebApplicationTrait; 27 | 28 | /** 29 | * The application's controller resolver. 30 | * 31 | * @var ControllerResolverInterface 32 | * @since 2.0.0 33 | */ 34 | protected $controllerResolver; 35 | 36 | /** 37 | * The application's router. 38 | * 39 | * @var RouterInterface 40 | * @since 2.0.0 41 | */ 42 | protected $router; 43 | 44 | /** 45 | * Class constructor. 46 | * 47 | * @param ControllerResolverInterface $controllerResolver The application's controller resolver 48 | * @param RouterInterface $router The application's router 49 | * @param Input|null $input An optional argument to provide dependency injection 50 | * for the application's input object. If the argument 51 | * is an Input object that object will become the 52 | * application's input object, otherwise a default input 53 | * object is created. 54 | * @param Registry|null $config An optional argument to provide dependency injection 55 | * for the application's config object. If the argument 56 | * is a Registry object that object will become the 57 | * application's config object, otherwise a default 58 | * config object is created. 59 | * @param WebClient|null $client An optional argument to provide dependency injection 60 | * for the application's client object. If the argument 61 | * is a Web\WebClient object that object will become the 62 | * application's client object, otherwise a default 63 | * client object is created. 64 | * @param ResponseInterface|null $response An optional argument to provide dependency injection 65 | * for the application's response object. If the 66 | * argument is a ResponseInterface object that object 67 | * will become the application's response object, 68 | * otherwise a default response object is created. 69 | * 70 | * @since 2.0.0 71 | */ 72 | public function __construct( 73 | ControllerResolverInterface $controllerResolver, 74 | RouterInterface $router, 75 | ?Input $input = null, 76 | ?Registry $config = null, 77 | ?WebClient $client = null, 78 | ?ResponseInterface $response = null 79 | ) { 80 | $this->controllerResolver = $controllerResolver; 81 | $this->router = $router; 82 | 83 | // Call the constructor as late as possible (it runs `initialise`). 84 | parent::__construct($input, $config, $client, $response); 85 | } 86 | 87 | /** 88 | * Method to run the application routines. 89 | * 90 | * @return void 91 | * 92 | * @since 2.0.0 93 | */ 94 | protected function doExecute(): void 95 | { 96 | $route = $this->router->parseRoute($this->get('uri.route'), $this->input->getMethod()); 97 | 98 | // Add variables to the input if not already set 99 | foreach ($route->getRouteVariables() as $key => $value) { 100 | $this->input->def($key, $value); 101 | } 102 | 103 | \call_user_func($this->controllerResolver->resolve($route)); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/WebApplicationInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @license GNU General Public License version 2 or later; see LICENSE 8 | */ 9 | 10 | namespace Joomla\Application; 11 | 12 | use Joomla\Input\Input; 13 | use Psr\Http\Message\ResponseInterface; 14 | 15 | /** 16 | * Application sub-interface defining a web application class 17 | * 18 | * @since 2.0.0 19 | */ 20 | interface WebApplicationInterface extends ApplicationInterface 21 | { 22 | /** 23 | * Method to get the application input object. 24 | * 25 | * @return Input 26 | * 27 | * @since 2.0.0 28 | */ 29 | public function getInput(): Input; 30 | 31 | /** 32 | * Redirect to another URL. 33 | * 34 | * If the headers have not been sent the redirect will be accomplished using a "301 Moved Permanently" or "303 See Other" code in the header 35 | * pointing to the new location. If the headers have already been sent this will be accomplished using a JavaScript statement. 36 | * 37 | * @param string $url The URL to redirect to. Can only be http/https URL 38 | * @param integer|boolean $status The HTTP status code to be provided. 303 is assumed by default. 39 | * 40 | * @return void 41 | * 42 | * @since 2.0.0 43 | * @throws \InvalidArgumentException 44 | */ 45 | public function redirect($url, $status = 303); 46 | 47 | /** 48 | * Set/get cachable state for the response. 49 | * 50 | * If $allow is set, sets the cachable state of the response. Always returns the current state. 51 | * 52 | * @param boolean $allow True to allow browser caching. 53 | * 54 | * @return boolean 55 | * 56 | * @since 2.0.0 57 | */ 58 | public function allowCache($allow = null); 59 | 60 | /** 61 | * Method to set a response header. 62 | * 63 | * If the replace flag is set then all headers with the given name will be replaced by the new one. 64 | * The headers are stored in an internal array to be sent when the site is sent to the browser. 65 | * 66 | * @param string $name The name of the header to set. 67 | * @param string $value The value of the header to set. 68 | * @param boolean $replace True to replace any headers with the same name. 69 | * 70 | * @return $this 71 | * 72 | * @since 2.0.0 73 | */ 74 | public function setHeader($name, $value, $replace = false); 75 | 76 | /** 77 | * Method to get the array of response headers to be sent when the response is sent to the client. 78 | * 79 | * @return array 80 | * 81 | * @since 2.0.0 82 | */ 83 | public function getHeaders(); 84 | 85 | /** 86 | * Method to clear any set response headers. 87 | * 88 | * @return $this 89 | * 90 | * @since 2.0.0 91 | */ 92 | public function clearHeaders(); 93 | 94 | /** 95 | * Send the response headers. 96 | * 97 | * @return $this 98 | * 99 | * @since 2.0.0 100 | */ 101 | public function sendHeaders(); 102 | 103 | /** 104 | * Set body content. If body content already defined, this will replace it. 105 | * 106 | * @param string $content The content to set as the response body. 107 | * 108 | * @return $this 109 | * 110 | * @since 2.0.0 111 | */ 112 | public function setBody($content); 113 | 114 | /** 115 | * Prepend content to the body content 116 | * 117 | * @param string $content The content to prepend to the response body. 118 | * 119 | * @return $this 120 | * 121 | * @since 2.0.0 122 | */ 123 | public function prependBody($content); 124 | 125 | /** 126 | * Append content to the body content 127 | * 128 | * @param string $content The content to append to the response body. 129 | * 130 | * @return $this 131 | * 132 | * @since 2.0.0 133 | */ 134 | public function appendBody($content); 135 | 136 | /** 137 | * Return the body content 138 | * 139 | * @return mixed The response body as a string. 140 | * 141 | * @since 2.0.0 142 | */ 143 | public function getBody(); 144 | 145 | /** 146 | * Get the PSR-7 Response Object. 147 | * 148 | * @return ResponseInterface 149 | * 150 | * @since 2.0.0 151 | */ 152 | public function getResponse(): ResponseInterface; 153 | 154 | /** 155 | * Check if the value is a valid HTTP status code 156 | * 157 | * @param integer $code The potential status code 158 | * 159 | * @return boolean 160 | * 161 | * @since 2.0.0 162 | */ 163 | public function isValidHttpStatus($code); 164 | 165 | /** 166 | * Set the PSR-7 Response Object. 167 | * 168 | * @param ResponseInterface $response The response object 169 | * 170 | * @return void 171 | * 172 | * @since 2.0.0 173 | */ 174 | public function setResponse(ResponseInterface $response): void; 175 | 176 | /** 177 | * Determine if we are using a secure (SSL) connection. 178 | * 179 | * @return boolean True if using SSL, false if not. 180 | * 181 | * @since 2.0.0 182 | */ 183 | public function isSslConnection(); 184 | } 185 | --------------------------------------------------------------------------------