PageRenderTime 65ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/index.php

https://bitbucket.org/fbertagnin/fbwork4
PHP | 2874 lines | 1506 code | 370 blank | 998 comment | 357 complexity | f3d42b366d37edb6d208f40583a74f9c MD5 | raw file
  1. <?php
  2. /**
  3. * Atomik Framework
  4. * Copyright (c) 2008-2009 Maxime Bouroumeau-Fuseau
  5. *
  6. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  7. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  8. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  9. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  10. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  11. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  12. * THE SOFTWARE.
  13. *
  14. * @package Atomik
  15. * @author Maxime Bouroumeau-Fuseau
  16. * @copyright 2008-2009 (c) Maxime Bouroumeau-Fuseau
  17. * @license http://www.opensource.org/licenses/mit-license.php
  18. * @link http://www.atomikframework.com
  19. */
  20. define('ATOMIK_VERSION', '2.2.2');
  21. !defined('ATOMIK_APP_ROOT') && define('ATOMIK_APP_ROOT', './app');
  22. /* -------------------------------------------------------------------------------------------
  23. * APPLICATION CONFIGURATION
  24. * ------------------------------------------------------------------------------------------ */
  25. Atomik::reset(array(
  26. 'app' => array(
  27. /* @var string */
  28. 'default_action' => 'index',
  29. /* The name of the layout
  30. * Add multiple layouts using an array (will be rendered in reverse order)
  31. * @var array|bool|string */
  32. 'layout' => false,
  33. /* @var bool */
  34. 'disable_layout' => false,
  35. /* An array where keys are route names and their value is an associative
  36. * array of default values
  37. * @see Atomik::route()
  38. * @var array */
  39. 'routes' => array(),
  40. /* @var bool */
  41. 'force_uri_extension' => false,
  42. /* List of escaping profiles where keys are profile names and their
  43. * value an array of callbacks
  44. * @see Atomik::escape()
  45. * @var array */
  46. 'escaping' => array(
  47. 'default' => array('htmlspecialchars', 'nl2br')
  48. ),
  49. /* @see Atomik::filter()
  50. * @var array */
  51. 'filters' => array(
  52. /* @var array */
  53. 'rules' => array(),
  54. /* @var array */
  55. 'callbacks' => array(),
  56. /* @var string */
  57. 'default_message' => 'The %s field failed to validate',
  58. /* @var string */
  59. 'required_message' => 'The %s field must be filled'
  60. ),
  61. /* @see Atomik::render()
  62. * @var array */
  63. 'views' => array(
  64. /* @var string */
  65. 'file_extension' => '.phtml',
  66. /* Alternative rendering engine
  67. * @see Atomik::_render()
  68. * @var callback */
  69. 'engine' => false,
  70. /* @var string */
  71. 'default_context' => 'html',
  72. /* The GET parameter to retrieve the current context
  73. * @var string */
  74. 'context_param' => 'format',
  75. /* List of contexts where keys are the context name.
  76. * Contexts can specify:
  77. * - prefix (string): the view filename's extension prefix
  78. * - layout (bool): whether the layout should be rendered
  79. * - content_type (string): the HTTP response content type
  80. * @var array */
  81. 'contexts' => array(
  82. 'html' => array(
  83. 'prefix' => '',
  84. 'layout' => true,
  85. 'content_type' => 'text/html'
  86. ),
  87. 'ajax' => array(
  88. 'prefix' => '',
  89. 'layout' => false,
  90. 'content_type' => 'text/html'
  91. ),
  92. 'xml' => array(
  93. 'prefix' => 'xml',
  94. 'layout' => false,
  95. 'content_type' => 'text/xml'
  96. ),
  97. 'json' => array(
  98. 'prefix' => 'json',
  99. 'layout' => false,
  100. 'content_type' => 'application/json'
  101. )
  102. )
  103. ),
  104. /* A parameter in the route that will allow to specify the http method
  105. * (override the request's method). False to disable
  106. * @var string */
  107. 'http_method_param' => '_method',
  108. /* @var array */
  109. 'allowed_http_methods' => array('GET', 'POST', 'PUT', 'DELETE', 'TRACE', 'HEAD', 'OPTIONS', 'CONNECT')
  110. )
  111. ));
  112. /* -------------------------------------------------------------------------------------------
  113. * CORE CONFIGURATION
  114. * ------------------------------------------------------------------------------------------ */
  115. Atomik::set(array(
  116. /* @var array */
  117. 'plugins' => array(),
  118. /* @var array */
  119. 'atomik' => array(
  120. /* Atomik's filename
  121. * @var string */
  122. 'scriptname' => __FILE__,
  123. /* Base url, set to null for auto detection
  124. * @var string */
  125. 'base_url' => null,
  126. /* Whether url rewriting is activated on the server
  127. * @var bool */
  128. 'url_rewriting' => false,
  129. /* @var bool */
  130. 'debug' => false,
  131. /* The GET parameter used to retreive the action
  132. * @var string */
  133. 'trigger' => 'action',
  134. /* Whether to register the class autoloader
  135. * @var bool */
  136. 'class_autoload' => true,
  137. /* @var bool */
  138. 'start_session' => true,
  139. /* Plugin's assets path template.
  140. * %s will be replaced by the plugin's name
  141. * @see Atomik::pluginAsset()
  142. * @var string */
  143. 'plugin_assets_tpl' => 'app/plugins/%s/assets/',
  144. /* @var array */
  145. 'log' => array(
  146. /* @var bool */
  147. 'register_default' => false,
  148. /* From which level to start logging messages
  149. * @var int */
  150. 'level' => LOG_WARNING,
  151. /* Message template for the default logger
  152. * @see Atomik::logToFile()
  153. * @var string */
  154. 'message_template' => '[%date%] [%level%] %message%'
  155. ),
  156. /* @var array */
  157. 'dirs' => array(
  158. 'app' => ATOMIK_APP_ROOT,
  159. 'plugins' => array(ATOMIK_APP_ROOT . '/modules', ATOMIK_APP_ROOT . '/plugins/'),
  160. 'actions' => ATOMIK_APP_ROOT . '/actions/',
  161. 'views' => ATOMIK_APP_ROOT . '/views/',
  162. 'layouts' => array(ATOMIK_APP_ROOT . '/layouts', ATOMIK_APP_ROOT . '/views'),
  163. 'helpers' => ATOMIK_APP_ROOT . '/helpers/',
  164. 'includes' => array(ATOMIK_APP_ROOT . '/includes/', ATOMIK_APP_ROOT . '/libraries/'),
  165. 'overrides' => ATOMIK_APP_ROOT . '/overrides/'
  166. ),
  167. /* @var array */
  168. 'files' => array(
  169. 'index' => 'index.php',
  170. 'config' => ATOMIK_APP_ROOT . '/config', // without extension
  171. 'bootstrap' => ATOMIK_APP_ROOT . '/bootstrap.php',
  172. 'pre_dispatch' => ATOMIK_APP_ROOT . '/pre_dispatch.php',
  173. 'post_dispatch' => ATOMIK_APP_ROOT . '/post_dispatch.php',
  174. '404' => ATOMIK_APP_ROOT . '/404.php',
  175. 'error' => ATOMIK_APP_ROOT . '/error.php',
  176. 'log' => ATOMIK_APP_ROOT . '/log.txt'
  177. ),
  178. /* @var bool */
  179. 'catch_errors' => false,
  180. /* @var bool */
  181. 'display_errors' => true,
  182. /* @var array */
  183. 'error_report_attrs' => array(
  184. 'atomik-error' => 'style="padding: 10px"',
  185. 'atomik-error-title' => 'style="font-size: 1.3em; font-weight: bold; color: #FF0000"',
  186. 'atomik-error-lines' => 'style="width: 100%; margin-bottom: 20px; background-color: #fff;'
  187. . 'border: 1px solid #000; font-size: 0.8em"',
  188. 'atomik-error-line' => '',
  189. 'atomik-error-line-error' => 'style="background-color: #ffe8e7"',
  190. 'atomik-error-line-number' => 'style="background-color: #eeeeee"',
  191. 'atomik-error-line-text' => '',
  192. 'atomik-error-stack' => ''
  193. )
  194. ),
  195. /* @var int */
  196. 'start_time' => time() + microtime()
  197. ));
  198. /* -------------------------------------------------------------------------------------------
  199. * CORE
  200. * ------------------------------------------------------------------------------------------ */
  201. // creates the A function (shortcut to Atomik::get)
  202. if (!function_exists('A')) {
  203. /**
  204. * Shortcut function to Atomik::get()
  205. * Useful when dealing with selectors
  206. *
  207. * @see Atomik::get()
  208. * @return mixed
  209. */
  210. function A()
  211. {
  212. $args = func_get_args();
  213. return call_user_func_array(array('Atomik', 'get'), $args);
  214. }
  215. }
  216. // starts Atomik unless ATOMIK_AUTORUN is set to false
  217. if (!defined('ATOMIK_AUTORUN') || ATOMIK_AUTORUN === true) {
  218. Atomik::run();
  219. }
  220. /**
  221. * Exception class for Atomik
  222. *
  223. * @package Atomik
  224. */
  225. class Atomik_Exception extends Exception {}
  226. /**
  227. * Atomik Framework Main class
  228. *
  229. * @package Atomik
  230. */
  231. final class Atomik
  232. {
  233. /**
  234. * Global store
  235. *
  236. * This property is used to stored all data accessed using get(), set()...
  237. *
  238. * @var array
  239. */
  240. public static $store = array();
  241. /**
  242. * Global store to reset to
  243. *
  244. * @var array
  245. */
  246. private static $reset = array();
  247. /**
  248. * Loaded plugins
  249. *
  250. * When a plugin is loaded, its name is saved in this array to
  251. * avoid loading it twice.
  252. *
  253. * @var array
  254. */
  255. private static $plugins = array();
  256. /**
  257. * Registered events
  258. *
  259. * The array keys are event names and their value is an array with
  260. * the event callbacks
  261. *
  262. * @var array
  263. */
  264. private static $events = array();
  265. /**
  266. * Selectors namespaces
  267. *
  268. * The array keys are the namespace name and the associated value is
  269. * the callback to call when the namespace is used
  270. *
  271. * @var array
  272. */
  273. private static $namespaces = array('flash' => array('Atomik', '_getFlashMessages'));
  274. /**
  275. * Execution contexts
  276. *
  277. * Each call to Atomik::execute() creates a context.
  278. *
  279. * @var array
  280. */
  281. private static $execContexts = array();
  282. /**
  283. * Pluggable applications
  284. *
  285. * @var array
  286. */
  287. private static $pluggableApplications = array();
  288. /**
  289. * Registered methods
  290. *
  291. * @var array
  292. */
  293. private static $methods = array();
  294. /**
  295. * Already loaded helpers
  296. *
  297. * @var array
  298. */
  299. private static $loadedHelpers = array();
  300. /**
  301. * Starts Atomik
  302. *
  303. * If dispatch is false, you will have to manually dispatch the request and exit.
  304. *
  305. * @param string $uri
  306. * @param bool $dispatch Whether to dispatch
  307. */
  308. public static function run($uri = null, $dispatch = true)
  309. {
  310. // wrap the whole app inside a try/catch block to catch all errors
  311. try {
  312. @chdir(dirname(A('atomik/scriptname')));
  313. // loads the config file
  314. if (file_exists($filename = self::get('atomik/files/config') . '.php')) {
  315. // PHP
  316. if (is_array($config = include($filename))) {
  317. self::set($config);
  318. }
  319. } else if (file_exists($filename = self::get('atomik/files/config') . '.ini')) {
  320. // INI
  321. if (($data = parse_ini_file($filename, true)) === false) {
  322. throw new Atomik_Exception('INI configuration malformed');
  323. }
  324. self::set(self::_dimensionizeArray($data, '.'), null, false);
  325. } else if (file_exists($filename = self::get('atomik/files/config') . '.json')) {
  326. // JSON
  327. if (($config = json_decode(file_get_contents($filename), true)) === null) {
  328. throw new Atomik_Exception('JSON configuration malformed');
  329. }
  330. self::set($config);
  331. }
  332. // adds includes dirs to php include path
  333. $includePaths = self::path(self::get('atomik/dirs/includes', array()), true);
  334. $includePaths[] = get_include_path();
  335. set_include_path(implode(PATH_SEPARATOR, $includePaths));
  336. // registers the error handler
  337. if (self::get('atomik/catch_errors', true) == true) {
  338. set_error_handler('Atomik::_errorHandler');
  339. }
  340. // sets the error reporting to all errors if debug mode is on
  341. if (self::get('atomik/debug', false) == true) {
  342. error_reporting(E_ALL | E_STRICT);
  343. }
  344. // default logger
  345. if (self::get('atomik/log/register_default', false) == true) {
  346. self::listenEvent('Atomik::Log', 'Atomik::logToFile');
  347. }
  348. // starts the session
  349. if (self::get('atomik/start_session', true) == true) {
  350. session_start();
  351. self::$store['session'] = &$_SESSION;
  352. }
  353. // registers the class autoload handler
  354. if (self::get('atomik/class_autoload', true) == true) {
  355. if (!function_exists('spl_autoload_register')) {
  356. throw new Atomik_Exception('Missing spl_autoload_register function');
  357. }
  358. spl_autoload_register('Atomik::autoload');
  359. }
  360. // cleans the plugins array
  361. $plugins = array();
  362. foreach (self::get('plugins', array()) as $key => $value) {
  363. if (!is_string($key)) {
  364. $key = $value;
  365. $value = array();
  366. }
  367. $plugins[ucfirst($key)] = (array) $value;
  368. }
  369. self::set('plugins', $plugins, false);
  370. // loads plugins
  371. // this method allows plugins that are being loaded to modify the plugins array
  372. $disabledPlugins = array();
  373. while (count($pluginsToLoad = array_diff(array_keys(self::get('plugins')), self::getLoadedPlugins(), $disabledPlugins)) > 0) {
  374. foreach ($pluginsToLoad as $plugin) {
  375. if (self::loadPlugin($plugin) === false) {
  376. $disabledPlugins[] = $plugin;
  377. }
  378. }
  379. }
  380. // loads bootstrap file
  381. if (file_exists($filename = self::get('atomik/files/bootstrap'))) {
  382. require($filename);
  383. }
  384. // core is starting
  385. self::fireEvent('Atomik::Start', array(&$cancel));
  386. if ($cancel) {
  387. self::end(true);
  388. }
  389. self::log('Starting', LOG_DEBUG);
  390. // checks if url rewriting is used
  391. if (!self::has('atomik/url_rewriting')) {
  392. self::set('atomik/url_rewriting', isset($_SERVER['REDIRECT_URL']) || isset($_SERVER['REDIRECT_URI']));
  393. }
  394. // dispatches
  395. if ($dispatch) {
  396. if (!self::dispatch($uri)) {
  397. self::trigger404();
  398. }
  399. // end
  400. self::end(true);
  401. }
  402. } catch (Exception $e) {
  403. self::log('Exception caught: ' . $e->getMessage(), LOG_ERR);
  404. // checks if we really want to catch errors
  405. if (!self::get('atomik/catch_errors', true)) {
  406. throw $e;
  407. }
  408. self::fireEvent('Atomik::Error', array($e));
  409. header('Location: ', false, 500);
  410. self::renderException($e);
  411. self::end(false);
  412. }
  413. }
  414. /**
  415. * Dispatches the request
  416. *
  417. * It takes an URI, applies routes, executes the action and renders the view.
  418. * If $uri is null, the value of the GET parameter specified as the trigger
  419. * will be used.
  420. *
  421. * @param string $uri
  422. * @param bool $allowPluggableApplication Whether to allow plugin application to be loaded
  423. */
  424. public static function dispatch($uri = null, $allowPluggableApplication = true)
  425. {
  426. self::fireEvent('Atomik::Dispatch::Start', array(&$uri, &$allowPluggableApplication, &$cancel));
  427. if ($cancel) {
  428. return true;
  429. }
  430. // checks if it's needed to auto discover the uri
  431. if ($uri === null) {
  432. // retreives the requested uri
  433. $trigger = self::get('atomik/trigger', 'action');
  434. if (isset($_GET[$trigger]) && !empty($_GET[$trigger])) {
  435. $uri = trim($_GET[$trigger], '/');
  436. }
  437. // retreives the base url
  438. if (self::get('atomik/base_url', null) === null) {
  439. if (self::get('atomik/url_rewriting') && (isset($_SERVER['REDIRECT_URL']) || isset($_SERVER['REDIRECT_URI']))) {
  440. // finds the base url from the redirected url
  441. $redirectUrl = isset($_SERVER['REDIRECT_URL']) ? $_SERVER['REDIRECT_URL'] : $_SERVER['REDIRECT_URI'];
  442. self::set('atomik/base_url', substr($redirectUrl, 0, -strlen($_GET[$trigger])));
  443. } else {
  444. // finds the base url from the script name
  445. self::set('atomik/base_url', rtrim(dirname($_SERVER['SCRIPT_NAME']), '/\\') . '/');
  446. }
  447. }
  448. } else {
  449. // sets the user defined request
  450. // retreives the base url
  451. if (self::get('atomik/base_url', null) === null) {
  452. // finds the base url from the script name
  453. self::set('atomik/base_url', rtrim(dirname($_SERVER['SCRIPT_NAME']), '/\\') . '/');
  454. }
  455. }
  456. // default uri
  457. if (empty($uri)) {
  458. $uri = self::get('app/default_action', 'index');
  459. }
  460. // routes the request
  461. if (($request = self::route($uri, $_GET)) === false) {
  462. return false;
  463. }
  464. // checking if no dot are in the action name to avoid any hack attempt and if no
  465. // underscore is use as first character in a segment
  466. if (strpos($request['action'], '..') !== false || substr($request['action'], 0, 1) == '_'
  467. || strpos($request['action'], '/_') !== false) {
  468. return false;
  469. }
  470. self::set('request_uri', $uri);
  471. self::set('request', $request);
  472. if (!self::has('full_request_uri')) {
  473. self::set('full_request_uri', $uri);
  474. }
  475. self::fireEvent('Atomik::Dispatch::Uri', array(&$uri, &$request, &$cancel));
  476. if ($cancel) {
  477. return true;
  478. }
  479. // checks if the uri triggers a pluggable application
  480. if ($allowPluggableApplication) {
  481. foreach (self::$pluggableApplications as $plugin => $pluggAppConfig) {
  482. if (!self::uriMatch($pluggAppConfig['route'], $uri)) {
  483. continue;
  484. }
  485. // rewrite uri
  486. $baseAction = trim($pluggAppConfig['route'], '/*');
  487. $uri = substr(trim($uri, '/'), strlen($baseAction));
  488. if ($uri == self::get('app/default_action')) {
  489. $uri = '';
  490. }
  491. self::set('atomik/base_action', $baseAction);
  492. // dispatches the pluggable application
  493. return self::dispatchPluggableApplication($plugin, $uri, $pluggAppConfig['config']);
  494. }
  495. }
  496. // fetches the http method
  497. $httpMethod = $_SERVER['REQUEST_METHOD'];
  498. if (($param = self::get('app/http_method_param', false)) !== false) {
  499. // checks if the route parameter to override the method is defined
  500. $httpMethod = self::get($param, $httpMethod, $request);
  501. }
  502. if (!in_array($httpMethod, self::get('app/allowed_http_methods'))) {
  503. // specified method not allowed
  504. return false;
  505. }
  506. self::set('app/http_method', strtoupper($httpMethod));
  507. // fetches the view context
  508. $viewContext = self::get(self::get('app/views/context_param', 'format'),
  509. self::get('app/views/default_context', 'html'), $request);
  510. self::set('app/view_context', $viewContext);
  511. // retreives view context params and prepare the response
  512. if (($viewContextParams = self::get('app/views/contexts/' . $viewContext, false)) !== false) {
  513. if ($viewContextParams['layout'] !== true) {
  514. self::set('app/layout', $viewContextParams['layout']);
  515. }
  516. header('Content-type: ' . self::get('content-type', 'text/html', $viewContextParams));
  517. }
  518. // configuration is ok, ready to dispatch
  519. self::fireEvent('Atomik::Dispatch::Before', array(&$cancel));
  520. if ($cancel) {
  521. return true;
  522. }
  523. self::log('Dispatching action ' . $request['action'], LOG_DEBUG);
  524. // pre dispatch action
  525. if (file_exists($filename = self::get('atomik/files/pre_dispatch'))) {
  526. require($filename);
  527. }
  528. // executes the action
  529. ob_start();
  530. if (($content = self::execute(self::get('request/action'), $viewContext, false)) === false) {
  531. return false;
  532. }
  533. $content = ob_get_clean() . $content;
  534. // renders the layouts if enable
  535. if (($layouts = self::get('app/layout', false)) !== false) {
  536. if (!empty($layouts) && !self::get('app/disable_layout', false)) {
  537. foreach (array_reverse((array) $layouts) as $layout) {
  538. $content = self::renderLayout($layout, $content);
  539. }
  540. }
  541. }
  542. // echoes the content
  543. self::fireEvent('Atomik::Output::Before', array(&$content));
  544. echo $content;
  545. self::fireEvent('Atomik::Output::After', array($content));
  546. // dispatch done
  547. self::fireEvent('Atomik::Dispatch::After');
  548. // post dispatch action
  549. if (file_exists($filename = self::get('atomik/files/post_dispatch'))) {
  550. require($filename);
  551. }
  552. return true;
  553. }
  554. /**
  555. * Checks if an uri matches the pattern. The pattern can contain the * wildcard at the
  556. * end to specify that it matches the target and all its child segments.
  557. *
  558. * @param string $pattern
  559. * @param string $uri Default is the current request uri
  560. * @return bool
  561. */
  562. public static function uriMatch($pattern, $uri = null)
  563. {
  564. if ($uri === null) {
  565. $uri = self::get('request_uri');
  566. }
  567. $uri = trim($uri, '/');
  568. $pattern = trim($pattern, '/');
  569. if (substr($pattern, -1) == '*') {
  570. $pattern = rtrim($pattern, '/*');
  571. return strlen($uri) >= strlen($pattern) && substr($uri, 0, strlen($pattern)) == $pattern;
  572. } else {
  573. return $uri == $pattern;
  574. }
  575. }
  576. /**
  577. * Parses an uri to extract parameters
  578. *
  579. * Routes defines how to extract parameters from an uri. They can
  580. * have additional default parameters.
  581. * There are two kind of routes:
  582. *
  583. * - segments:
  584. * the uri is divided into path segments. Each segment can be
  585. * either static or a parameter (indicated by :).
  586. * eg: /archives/:year/:month
  587. *
  588. * - regexp:
  589. * uses a regexp against the uri. Must be enclosed using # instead of
  590. * slashes parameters must be specified as named subpattern.
  591. * eg: #^archives/(?P<year>[0-9]{4})/(?P<month>[0-9]{2})$#
  592. *
  593. * If no route matches, the default route (ie :action) will automatically be used.
  594. * Unless you're using regexps, any additional segments will be added as parameters
  595. * eg: /archives/2009/01/id/1, will also have the id=1 parameter
  596. *
  597. * You can also name your routes using the @name parameter (which won't be included
  598. * in the returned params). Named route can then be use with Atomik::url()
  599. *
  600. * @param string $uri
  601. * @param array $params Additional parameters which are not in the uri
  602. * @param array $routes Uses app/routes if null
  603. * @return array|boolean Route parameters or false if it fails
  604. */
  605. public static function route($uri, $params = array(), $routes = null)
  606. {
  607. if ($routes === null) {
  608. $routes = self::get('app/routes');
  609. }
  610. self::fireEvent('Atomik::Router::Start', array(&$uri, &$routes, &$params));
  611. // extracts uri information
  612. $components = parse_url($uri);
  613. $uri = trim($components['path'], '/');
  614. $uriSegments = explode('/', $uri);
  615. $uriExtension = false;
  616. if (isset($components['query'])) {
  617. parse_str($components['query'], $query);
  618. $params = array_merge($query, $params);
  619. }
  620. // extract the file extension from the uri
  621. $lastSegment = array_pop($uriSegments);
  622. if (($dot = strrpos($lastSegment, '.')) !== false) {
  623. $uriExtension = substr($lastSegment, $dot + 1);
  624. $lastSegment = substr($lastSegment, 0, $dot);
  625. }
  626. $uriSegments[] = $lastSegment;
  627. // checks if the extension must be present
  628. if (self::get('app/force_uri_extension', false) && $uriExtension === false) {
  629. return false;
  630. }
  631. // searches for a route matching the uri
  632. $found = false;
  633. $request = array();
  634. foreach ($routes as $route => $default) {
  635. if (!is_string($route)) {
  636. $route = $default;
  637. $default = array();
  638. }
  639. // removes the route name from the default params
  640. if (isset($default['@name'])) {
  641. unset($default['@name']);
  642. }
  643. // regexp
  644. if ($route{0} == '#') {
  645. if (!preg_match($route, $uri, $matches)) {
  646. continue;
  647. }
  648. unset($matches[0]);
  649. $found = true;
  650. $request = array_merge($default, $matches);
  651. break;
  652. }
  653. $valid = true;
  654. $segments = explode('/', trim($route, '/'));
  655. $request = $default;
  656. $extension = false;
  657. // extract the file extension from the route
  658. $lastSegment = array_pop($segments);
  659. if (($dot = strrpos($lastSegment, '.')) !== false) {
  660. $extension = substr($lastSegment, $dot + 1);
  661. $lastSegment = substr($lastSegment, 0, $dot);
  662. }
  663. $segments[] = $lastSegment;
  664. // checks the extension
  665. if ($extension !== false) {
  666. if ($extension{0} == ':') {
  667. // extension is a parameter
  668. if ($uriExtension !== false) {
  669. $request[substr($extension, 1)] = $uriExtension;
  670. } else if (!isset($request[substr($extension, 1)])) {
  671. // no uri extension and no default value
  672. continue;
  673. }
  674. } else if ($extension != $uriExtension) {
  675. continue;
  676. }
  677. }
  678. for ($i = 0, $count = count($segments); $i < $count; $i++) {
  679. if (substr($segments[$i], 0, 1) == ':') {
  680. // segment is a parameter
  681. if (isset($uriSegments[$i])) {
  682. // this segment is defined in the uri
  683. $request[substr($segments[$i], 1)] = $uriSegments[$i];
  684. $segments[$i] = $uriSegments[$i];
  685. } else if (!array_key_exists(substr($segments[$i], 1), $default)) {
  686. // not defined in the uri and no default value
  687. $valid = false;
  688. break;
  689. }
  690. } else {
  691. // fixed segment
  692. if (!isset($uriSegments[$i]) || $uriSegments[$i] != $segments[$i]) {
  693. $valid = false;
  694. break;
  695. }
  696. }
  697. }
  698. // checks if route is valid and if the action param is set
  699. if ($valid && isset($request['action'])) {
  700. $found = true;
  701. // if there's remaining segments in the uri, adding them as params
  702. if (($count = count($uriSegments)) > ($start = count($segments))) {
  703. for ($i = $start; $i < $count; $i += 2) {
  704. if (isset($uriSegments[$i + 1])) {
  705. $request[$uriSegments[$i]] = $uriSegments[$i + 1];
  706. }
  707. }
  708. }
  709. break;
  710. }
  711. }
  712. if (!$found) {
  713. // route not found, creating default route
  714. $request = array(
  715. 'action' => implode('/', $uriSegments),
  716. self::get('app/views/context_param', 'format') => $uriExtension === false ?
  717. self::get('app/views/default_context', 'html') : $uriExtension
  718. );
  719. }
  720. $request = array_merge($params, $request);
  721. self::fireEvent('Atomik::Router::End', array($uri, &$request));
  722. return $request;
  723. }
  724. /**
  725. * Executes an action
  726. *
  727. * Searches for a file called after the action (with the php extension) inside
  728. * directories set under atomik/dirs/actions. If no file is found, it will search
  729. * for a view and render it. If neither of them are found, it will throw an exception.
  730. *
  731. * @see Atomik::render()
  732. * @param string $action The action name. The HTTP method can be prefixed after a dot
  733. * @param bool|string $viewContext The view context. Set to false to not render the view and return the variables or to true for the request's context
  734. * @param bool $triggerError Whether to throw an exception if an error occurs
  735. * @return mixed The output of the view or an array of variables or false if an error occured
  736. */
  737. public static function execute($action, $viewContext = true, $triggerError = true)
  738. {
  739. $view = $action;
  740. $vars = array();
  741. $render = $viewContext !== false;
  742. if (is_bool($viewContext)) {
  743. // using the request's context
  744. $viewContext = self::get('app/view_context');
  745. }
  746. // appends the context's prefix to the view name
  747. $prefix = self::get('app/views/contexts/' . $viewContext . '/prefix', $viewContext);
  748. if (!empty($prefix)) {
  749. $view .= '.' . $prefix;
  750. }
  751. // creates the execution context
  752. $context = array('action' => &$action, 'view' => &$view, 'render' => &$render);
  753. self::$execContexts[] =& $context;
  754. self::fireEvent('Atomik::Execute::Start', array(&$action, &$context, &$triggerError));
  755. if ($action === false) {
  756. return false;
  757. }
  758. // checks if the method is specified in $action
  759. if (($dot = strrpos($action, '.')) !== false) {
  760. // it is, extract it
  761. $methodAction = $action;
  762. $method = substr($action, $dot + 1);
  763. $action = substr($action, 0, $dot);
  764. } else {
  765. // use the current request's http method
  766. $method = strtolower(self::get('app/http_method'));
  767. $methodAction = $action . '.' . $method;
  768. }
  769. // filenames
  770. $actionFilename = self::actionFilename($action);
  771. $methodActionFilename = self::actionFilename($methodAction);
  772. $viewFilename = self::viewFilename($view);
  773. // checks if at least one of the action files or the view file is defined
  774. if ($actionFilename === false && $methodActionFilename === false && $viewFilename === false) {
  775. if ($triggerError) {
  776. throw new Atomik_Exception('Action ' . $action . ' does not exist');
  777. }
  778. return false;
  779. }
  780. if ($viewFilename === false) {
  781. // no view files, disabling view
  782. $view = false;
  783. }
  784. self::fireEvent('Atomik::Execute::Before', array(&$action, &$context, &$actionFilename, &$methodActionFilename, &$triggerError));
  785. // class name if using a class
  786. $className = str_replace(' ', '_', ucwords(str_replace('/', ' ', $action)));
  787. // executes the global action
  788. if ($actionFilename !== false) {
  789. // executes the action in its own scope and fetches defined variables
  790. $vars = self::executeFile($actionFilename, array(), $className . 'Action');
  791. }
  792. // executes the method specific action
  793. if ($methodActionFilename !== false) {
  794. // executes the action in its own scope and fetches defined variables
  795. $vars = self::executeFile($methodActionFilename, $vars, $className . ucfirst($method) . 'Action');
  796. }
  797. self::fireEvent('Atomik::Execute::After', array($action, &$context, &$vars, &$triggerError));
  798. // deletes the execution context
  799. array_pop(self::$execContexts);
  800. // returns $vars if the view should not be rendered
  801. if ($render === false) {
  802. return $vars;
  803. }
  804. // no view
  805. if ($view === false) {
  806. return '';
  807. }
  808. // renders the view associated to the action
  809. return self::render($view, $vars, $triggerError);
  810. }
  811. /**
  812. * Executes the action file inside a clean scope and returns defined variables
  813. *
  814. * @see Atomik::_execute()
  815. * @param string $actionFilename
  816. * @param array $vars Variables that will be available in the scope
  817. * @param string $className If a class name is specified, it will try to execute its execute() static method
  818. * @return array
  819. */
  820. public static function executeFile($actionFilename, $vars = array(), $className = null)
  821. {
  822. self::fireEvent('Atomik::Executefile', array(&$actionFilename, &$vars));
  823. $atomik = new self();
  824. $vars = $atomik->_execute($actionFilename, $vars, $className);
  825. $atomik = null;
  826. return $vars;
  827. }
  828. /**
  829. * Executes a file inside a clean scope and returns defined variables
  830. *
  831. * @internal
  832. * @param string $__filename Filename
  833. * @param array $__vars An array containing key/value pairs that will be transformed to variables accessible inside the file
  834. * @return string View output
  835. */
  836. private function _execute($__filename, $__vars = array(), $__className = null)
  837. {
  838. extract($__vars);
  839. require($__filename);
  840. $vars = array();
  841. // checks if a class is used
  842. if ($__className !== null && class_exists($__className, false) && method_exists($__className, 'execute')) {
  843. // call the class execute() static method
  844. if (($vars = call_user_func(array($__className, 'execute'))) === null) {
  845. $vars = array();
  846. }
  847. $vars = array_merge(get_class_vars($__className), $vars);
  848. }
  849. // retreives "public" variables (not prefixed with an underscore)
  850. $definedVars = get_defined_vars();
  851. foreach ($definedVars as $name => $value) {
  852. if (substr($name, 0, 1) != '_') {
  853. $vars[$name] = $value;
  854. }
  855. }
  856. return $vars;
  857. }
  858. /**
  859. * Prevents the view of the actionfrom which it's called to be rendered
  860. */
  861. public static function noRender()
  862. {
  863. if (count(self::$execContexts)) {
  864. self::$execContexts[count(self::$execContexts) - 1]['view'] = false;
  865. }
  866. }
  867. /**
  868. * Modifies the view associted to the action from which it's called
  869. *
  870. * @param string $view View name
  871. */
  872. public static function setView($view)
  873. {
  874. if (count(self::$execContexts)) {
  875. self::$execContexts[count(self::$execContexts) - 1]['view'] = $view;
  876. }
  877. }
  878. /**
  879. * Renders a view
  880. *
  881. * Searches for a file called after the view inside
  882. * directories configured in atomik/dirs/views. If no file is found, an
  883. * exception is thrown unless $triggerError is false.
  884. *
  885. * @param string $view The view name
  886. * @param array $vars An array containing key/value pairs that will be transformed to variables accessible inside the view
  887. * @param bool $triggerError Whether to throw an exception if an error occurs
  888. * @param array $dirs Directories where view files are stored
  889. * @return string|bool
  890. */
  891. public static function render($view, $vars = array(), $triggerError = true, $dirs = null)
  892. {
  893. if ($dirs === null) {
  894. $dirs = self::get('atomik/dirs/views');
  895. }
  896. self::fireEvent('Atomik::Render::Start', array(&$view, &$vars, &$dirs, &$triggerError));
  897. // view filename
  898. if (($filename = self::viewFilename($view, $dirs)) === false) {
  899. if ($triggerError) {
  900. throw new Atomik_Exception('View ' . $view . ' not found');
  901. }
  902. return false;
  903. }
  904. self::fireEvent('Atomik::Render::Before', array(&$view, &$vars, &$filename, $triggerError));
  905. $output = self::renderFile($filename, $vars);
  906. self::fireEvent('Atomik::Render::After', array($view, &$output, $vars, $filename, $triggerError));
  907. return $output;
  908. }
  909. /**
  910. * Renders a file using a filename which will not be resolved.
  911. *
  912. * @param string $filename Filename
  913. * @param array $vars An array containing key/value pairs that will be transformed to variables accessible inside the file
  914. * @return string The output of the rendered file
  915. */
  916. public static function renderFile($filename, $vars = array())
  917. {
  918. self::fireEvent('Atomik::Renderfile::Before', array(&$filename, &$vars));
  919. if (($callback = self::get('app/views/engine', false)) !== false) {
  920. if (!is_callable($callback)) {
  921. throw new Atomik_Exception('The specified rendering engine callback cannot be called');
  922. }
  923. $output = $callback($filename, $vars);
  924. } else {
  925. $atomik = new self();
  926. $output = $atomik->_render($filename, $vars);
  927. $atomik = null;
  928. }
  929. self::fireEvent('Atomik::Renderfile::After', array($filename, &$output, $vars));
  930. return $output;
  931. }
  932. /**
  933. * Renders a layout
  934. *
  935. * @param string $layout Layout name
  936. * @param string $content The content that will be available in the layout in the $contentForLayout variable
  937. * @param array $vars An array containing key/value pairs that will be transformed to variables accessible inside the layout
  938. * @param bool $triggerError Whether to throw an exception if an error occurs
  939. * @param array $dirs Directories where to search for layouts
  940. * @return string
  941. */
  942. public static function renderLayout($layout, $content, $vars = array(), $triggerError = true, $dirs = null)
  943. {
  944. if ($dirs === null) {
  945. $dirs = self::get('atomik/dirs/layouts');
  946. }
  947. self::fireEvent('Atomik::Renderlayout', array(&$layout, &$content, &$vars, &$triggerError, &$dirs));
  948. $vars['contentForLayout'] = $content;
  949. return self::render($layout, $vars, $triggerError, $dirs);
  950. }
  951. /**
  952. * Renders a file (internal/default rendering engine)
  953. *
  954. * @internal
  955. * @param string $__filename Filename
  956. * @param array $__vars An array containing key/value pairs that will be transformed to variables accessible inside the file
  957. * @return string View output
  958. */
  959. private function _render($__filename, $__vars = array())
  960. {
  961. extract($__vars);
  962. ob_start();
  963. include($__filename);
  964. return ob_get_clean();
  965. }
  966. /**
  967. * Loads an helper file
  968. *
  969. * @param string $helperName
  970. * @param array $dirs Directories where to search for helpers
  971. */
  972. public static function loadHelper($helperName, $dirs = null)
  973. {
  974. if (isset(self::$loadedHelpers[$helperName])) {
  975. return;
  976. }
  977. if ($dirs === null) {
  978. $dirs = self::get('atomik/dirs/helpers');
  979. }
  980. self::fireEvent('Atomik::Loadhelper::Before', array(&$helperName, &$dirs));
  981. if (($filename = self::path($helperName . '.php', $dirs)) === false) {
  982. throw new Atomik_Exception('Helper ' . $helperName . ' not found');
  983. }
  984. include $filename;
  985. if (!function_exists($helperName)) {
  986. // searching for an helper defined as a class
  987. $camelizedHelperName = str_replace(' ', '', ucwords(str_replace('_', ' ', $helperName)));
  988. $className = $camelizedHelperName . 'Helper';
  989. if (!class_exists($className, false)) {
  990. // neither a function nor a class has been found
  991. throw new Exception('Helper ' . $helperName . ' file found but no function or class matching the helper name');
  992. }
  993. // helper defined as a class
  994. self::$loadedHelpers[$helperName] = array(new $className(), $camelizedHelperName);
  995. } else {
  996. // helper defined as a function
  997. self::$loadedHelpers[$helperName] = $helperName;
  998. }
  999. self::fireEvent('Atomik::Loadhelper::After', array($helperName, $dirs));
  1000. }
  1001. /**
  1002. * Executes an helper
  1003. *
  1004. * @param string $helperName
  1005. * @param array $args Arguments for the helper
  1006. * @param array $dirs Directories where to search for helpers
  1007. * @return mixed
  1008. */
  1009. public static function helper($helperName, $args = array(), $dirs = null)
  1010. {
  1011. self::loadHelper($helperName, $dirs);
  1012. return call_user_func_array(self::$loadedHelpers[$helperName], $args);
  1013. }
  1014. /**
  1015. * PHP magic method to handle calls to helper in views
  1016. *
  1017. * @param string $helperName
  1018. * @param array $args
  1019. * @return mixed
  1020. */
  1021. public function __call($helperName, $args)
  1022. {
  1023. if (method_exists('Atomik', $helperName)) {
  1024. return call_user_func_array(array('Atomik', $helperName), $args);
  1025. }
  1026. if (isset(self::$methods[$helperName])) {
  1027. return call_user_func_array(self::$methods[$helperName], $args);
  1028. }
  1029. return self::helper($helperName, $args);
  1030. }
  1031. /**
  1032. * Disables the layout
  1033. *
  1034. * @param bool $disable Whether to disable the layout
  1035. */
  1036. public static function disableLayout($disable = true)
  1037. {
  1038. self::set('app/disable_layout', $disable);
  1039. }
  1040. /**
  1041. * Fires the Atomik::End event and exits the application
  1042. *
  1043. * @param bool $success Whether the application exit on success or because an error occured
  1044. * @param bool $writeSession Whether to call session_write_close() before exiting
  1045. */
  1046. public static function end($success = false, $writeSession = true)
  1047. {
  1048. self::fireEvent('Atomik::End', array($success, &$writeSession));
  1049. if ($writeSession) {
  1050. session_write_close();
  1051. }
  1052. self::log('Ending', LOG_DEBUG);
  1053. exit;
  1054. }
  1055. /* -------------------------------------------------------------------------------------------
  1056. * Accessors
  1057. * ------------------------------------------------------------------------------------------ */
  1058. /**
  1059. * Sets a key/value pair in the store
  1060. *
  1061. * If the first argument is an array, values are merged recursively.
  1062. * The array is first dimensionized
  1063. * You can set values from sub arrays by using a path-like key.
  1064. * For example, to set the value inside the array $array[key1][key2]
  1065. * use the key 'key1/key2'
  1066. * Can be used on any array by specifying the third argument
  1067. *
  1068. * @see Atomik::_dimensionizeArray()
  1069. * @param array|string $key Can be an array to set many key/value
  1070. * @param mixed $value
  1071. * @param bool $dimensionize Whether to use Atomik::_dimensionizeArray() on $key
  1072. * @param array $array The array on which the operation is applied
  1073. * @param array $add Whether to add values or replace them
  1074. */
  1075. public static function set($key, $value = null, $dimensionize = true, &$array = null, $add = false)
  1076. {
  1077. // if $data is null, uses the global store
  1078. if ($array === null) {
  1079. $array = &self::$store;
  1080. }
  1081. // setting a key directly
  1082. if (is_string($key)) {
  1083. $parentArrayKey = strpos($key, '/') !== false ? dirname($key) : null;
  1084. $key = basename($key);
  1085. $parentArray = &self::getRef($parentArrayKey, $array);
  1086. if ($parentArray === null) {
  1087. $dimensionizedParentArray = self::_dimensionizeArray(array($parentArrayKey => null));
  1088. $array = self::_mergeRecursive($array, $dimensionizedParentArray);
  1089. $parentArray = &self::getRef($parentArrayKey, $array);
  1090. }
  1091. if ($add !== false) {
  1092. if (!isset($parentArray[$key]) || $parentArray[$key] === null) {
  1093. if (!is_array($value)) {
  1094. $parentArray[$key] = $value;
  1095. return;
  1096. }
  1097. $parentArray[$key] = array();
  1098. } else if (!is_array($parentArray[$key])) {
  1099. $parentArray[$key] = array($parentArray[$key]);
  1100. }
  1101. $value = is_array($value) ? $value : array($value);
  1102. if ($add == 'prepend') {
  1103. $parentArray[$key] = array_merge_recursive($value, $parentArray[$key]);
  1104. } else {
  1105. $parentArray[$key] = array_merge_recursive($parentArray[$key], $value);
  1106. }
  1107. } else {
  1108. $parentArray[$key] = $value;
  1109. }
  1110. return;
  1111. }
  1112. if (!is_array($key)) {
  1113. throw new Atomik_Exception('The first parameter of Atomik::set() must be a string or an array, ' . gettype($key) . ' given');
  1114. }
  1115. if ($dimensionize) {
  1116. $key = self::_dimensionizeArray($key);
  1117. }
  1118. // merges the store and the array
  1119. if ($add) {
  1120. $array = array_merge_recursive($array, $key);
  1121. } else {
  1122. $array = self::_mergeRecursive($array, $key);
  1123. }
  1124. }
  1125. /**
  1126. * Adds a value to the array pointed by the key
  1127. *
  1128. * If the first argument is an array, values are merged recursively.
  1129. * The array is first dimensionized
  1130. * You can add values to sub arrays by using a path-like key.
  1131. * For example, to add a value to the array $array[key1][key2]
  1132. * use the key 'key1/key2'
  1133. * If the value pointed by the key is not an array, it will be
  1134. * transformed to one.
  1135. * Can be used on any array by specifying the third argument
  1136. *
  1137. * @see Atomik::_dimensionizeArray()
  1138. * @param array|string $key Can be an array to add many key/value
  1139. * @param mixed $value
  1140. * @param bool $dimensionize Whether to use Atomik::_dimensionizeArray()
  1141. * @param array $array The array on which the operation is applied
  1142. */
  1143. public static function add($key, $value = null, $dimensionize = true, &$array = null)
  1144. {
  1145. return self::set($key, $value, $dimensionize, $array, 'append');
  1146. }
  1147. /**
  1148. * Prependes a value to the array pointed by the key
  1149. *
  1150. * Works the same as add()
  1151. *
  1152. * @see Atomik::add()
  1153. * @param array|string $key Can be an array to add many key/value
  1154. * @param mixed $value
  1155. * @param bool $dimensionize Whether to use Atomik::_dimensionizeArray()
  1156. * @param array $array The array on which the operation is applied
  1157. */
  1158. public static function prepend($key, $value = null, $dimensionize = true, &$array = null)
  1159. {
  1160. return self::set($key, $value, $dimensionize, $array, 'prepend');
  1161. }
  1162. /**
  1163. * Like array_merge() but recursively
  1164. *
  1165. * @internal
  1166. * @see array_merge()
  1167. * @param array $array1
  1168. * @param array $array2
  1169. * @return array
  1170. */
  1171. public static function _mergeRecursive($array1, $array2)
  1172. {
  1173. $array = $array1;
  1174. foreach ($array2 as $key => $value) {
  1175. if (is_array($value) && array_key_exists($key, $array1) && is_array($array1[$key])) {
  1176. $array[$key] = self::_mergeRecursive($array1[$key], $value);
  1177. continue;
  1178. }
  1179. $array[$key] = $value;
  1180. }
  1181. return $array;
  1182. }
  1183. /**
  1184. * Recursively checks array for path-like keys (ie. keys containing slashes)
  1185. * and transform them into multi dimensions array
  1186. *
  1187. * @internal
  1188. * @param array $array
  1189. * @param string $separator
  1190. * @return array
  1191. */
  1192. public static function _dimensionizeArray($array, $separator = '/')
  1193. {
  1194. $dimArray = array();
  1195. foreach ($array as $key => $value) {
  1196. // checks if the key is a path
  1197. if (strpos($key, $separator) !== false) {
  1198. $parts = explode($separator, $key);
  1199. $firstPart = array_shift($parts);
  1200. // recursively dimensionize the key
  1201. $value = self::_dimensionizeArray(array(implode($separator, $parts) => $value), $separator);
  1202. if (isset($dimArray[$firstPart])) {
  1203. if (!is_array($dimArray[$firstPart])) {
  1204. // if $firstPart exists but is not an array, drops the value and use an array
  1205. $dimArray[$firstPart] = array();
  1206. }
  1207. // merge recursively both arrays
  1208. $dimArray[$firstPart] = self::_mergeRecursive($dimArray[$firstPart], $value);
  1209. } else {
  1210. $dimArray[$firstPart] = $value;
  1211. }
  1212. } else if (is_array($value)) {
  1213. // dimensionize sub arrays
  1214. $value = self::_dimensionizeArray($value, $separator);
  1215. if (isset($dimArray[$key])) {
  1216. $dimArray[$key] = self::_mergeRecursive($dimArray[$key], $value);
  1217. } else {
  1218. $dimArray[$key] = $value;
  1219. }
  1220. } else {
  1221. $dimArray[$key] = $value;
  1222. }
  1223. }
  1224. return $dimArray;
  1225. }
  1226. /**
  1227. * Gets a value using its associatied key from the store
  1228. *
  1229. * You can fetch value from sub arrays by using a path-like
  1230. * key. Separate each key with a slash. For example if you want
  1231. * to fetch the value from an $store[key1][key2][key3] you can use
  1232. * key1/key2/key3
  1233. * Can be used on any array by specifying the third argument
  1234. *
  1235. * @param string|array $key The configuration key which value should be returned. If null, fetches all values
  1236. * @param mixed $default Default value if the key is not found
  1237. * @param array $array The array on which the operation is applied
  1238. * @return mixed
  1239. */
  1240. public static function get($key = null, $default = null, $array = null)
  1241. {
  1242. // checks if a namespace is used
  1243. if (is_string($key) && preg_match('/^([a-z]+):(.*)/', $key, $match)) {
  1244. // checks if the namespace exists */
  1245. if (isset(self::$namespaces[$match[1]])) {
  1246. // calls the namespace callback and returns
  1247. $args = func_get_args();
  1248. $args[0] = $match[2];
  1249. return call_user_func_array(self::$namespaces[$match[1]], $args);
  1250. }
  1251. }
  1252. if (($value = self::getRef($key, $array)) !== null) {
  1253. return $value;
  1254. }
  1255. // key not found, returns default
  1256. return $default;
  1257. }
  1258. /**
  1259. * Checks if a key is defined in the store
  1260. *
  1261. * Can check through sub array using a path-like key
  1262. * Can be used on any array by specifying the second argument
  1263. *
  1264. * @see Atomik::get()
  1265. * @param string $key The key which should be deleted
  1266. * @param array $array The array on which the operation is applied
  1267. * @return bool
  1268. */
  1269. public static function has($key, $array = null)
  1270. {
  1271. return self::getRef($key, $array) !== null;
  1272. }
  1273. /**
  1274. * Deletes a key from the store
  1275. *
  1276. * Can delete through sub array using a path-like key
  1277. * Can be used on any array by specifying the second argument
  1278. *
  1279. * @see Atomik::get()
  1280. * @param string $key
  1281. * @param array $array The array on which the operation is applied
  1282. * @return mixed The deleted value
  1283. */
  1284. public static function delete($key, &$array = null)
  1285. {
  1286. $parentArrayKey = strpos($key, '/') !== false ? dirname($key) : null;
  1287. $key = basename($key);
  1288. $parentArray = &self::getRef($parentArrayKey, $array);
  1289. if ($parentArray === null || !array_key_exists($key, $parentArray)) {
  1290. throw new Atomik_Exception('Key "' . $key . '" does not exists');
  1291. }
  1292. $value = $parentArray[$key];
  1293. unset($parentArray[$key]);
  1294. return $value;
  1295. }
  1296. /**
  1297. * Gets a reference to a value from the store using its associatied key
  1298. *
  1299. * You can fetch value from sub arrays by using a path-like
  1300. * key. Separate each key with a slash. For example if you want
  1301. * to fetch the value from an $store[key1][key2][key3] you can use
  1302. * key1/key2/key3
  1303. * Can be used on any array by specifying the second argument
  1304. *
  1305. * @param string|array $key The configuration key which value should be returned. If null, fetches all values
  1306. * @param array $array The array on which the operation is applied
  1307. * @return mixed Null if the key does not match
  1308. */
  1309. public static function &getRef($key = null, &$array = null)
  1310. {
  1311. $null = null;
  1312. // returns the store
  1313. if ($array === null) {
  1314. $array = &self::$store;
  1315. }
  1316. // return the whole arrat
  1317. if ($key === null) {
  1318. return $array;
  1319. }
  1320. // checks if the $key is an array
  1321. if (!is_array($key)) {
  1322. // checks if it has slashes
  1323. if (!strpos($key, '/')) {
  1324. if (array_key_exists($key, $array)) {
  1325. $value =& $array[$key];
  1326. return $value;
  1327. }
  1328. return $null;
  1329. }
  1330. // creates an array by spliting using slashes
  1331. $key = explode('/', $key);
  1332. }
  1333. // checks if the key exists
  1334. $firstKey = array_shift($key);
  1335. if (array_key_exists($firstKey, $array)) {
  1336. if (count($key) > 0) {
  1337. // there's still keys so it goes deeper
  1338. return self::getRef($key, $array[$firstKey]);
  1339. } else {
  1340. // the key has been found
  1341. $value =& $array[$firstKey];
  1342. return $value;
  1343. }
  1344. }
  1345. return $null;
  1346. }
  1347. /**
  1348. * Resets the global store
  1349. *
  1350. * If no argument are specified the store is resetted, otherwise value are set normally and the
  1351. * state is saved.
  1352. *
  1353. * @internal
  1354. * @see Atomik::set()
  1355. * @param array|string $key Can be an array to set many key/value
  1356. * @param mixed $value
  1357. * @param bool $dimensionize Whether to use Atomik::_dimensionizeArray() on $key
  1358. */
  1359. public static function reset($key = null, $value = null, $dimensionize = true)
  1360. {
  1361. if ($key !== null) {
  1362. self::set($key, $value, $dimensionize, self::$reset);
  1363. self::set($key, $value, $dimensionize);
  1364. return;
  1365. }
  1366. // reset
  1367. self::$store = self::_mergeRecursive(self::$store, self::$reset);
  1368. }
  1369. /**
  1370. * Registers a new selector namespace
  1371. *
  1372. * A namespace preceed a key. When used, $callback will be
  1373. * called instead of the normal logic. Applies only on get() calls.
  1374. *
  1375. * @param string $namespace
  1376. * @param callback $callback
  1377. */
  1378. public static function registerSelector($namespace, $callback)
  1379. {
  1380. self::$namespaces[$namespace] = $callback;
  1381. }
  1382. /* -------------------------------------------------------------------------------------------
  1383. * Plugins
  1384. * ------------------------------------------------------------------------------------------ */
  1385. /**
  1386. * Loads a plugin using the configuration specified under plugins
  1387. *
  1388. * @param string $name
  1389. * @return bool
  1390. */
  1391. public static function loadPlugin($name)
  1392. {
  1393. $name = ucfirst($name);
  1394. if (($config = self::get('plugins/' . $name, array())) === false) {
  1395. return false;
  1396. }
  1397. return self::loadCustomPlugin($name, $config);
  1398. }
  1399. /**
  1400. * Loads a custom plugin
  1401. *
  1402. * Options:
  1403. * - dirs: Directories from where to load the plugin
  1404. * - classNameTemplate: % will be replaced with the plugin name
  1405. * - callStart: Whether to call the start() method on the plugin class
  1406. *
  1407. * @param string $plugin The plugin name
  1408. * @param array $config Configuration for this plugin
  1409. * @param array $options Options for loading this plugin
  1410. * @return bool Success
  1411. */
  1412. public static function loadCustomPlugin($plugin, $config = array(), $options = array())
  1413. {
  1414. $plugin = ucfirst($plugin);
  1415. $defaultOptions = array(
  1416. 'dirs' => null,
  1417. 'classNameTemplate' => '%Plugin',
  1418. 'callStart' => true
  1419. );
  1420. $options = array_merge($defaultOptions, $options);
  1421. // checks if the plugin is already loaded
  1422. if (self::isPluginLoaded($plugin)) {
  1423. return true;
  1424. }
  1425. // use default directories
  1426. if ($options['dirs'] === null) {
  1427. $options['dirs'] = self::get('atomik/dirs/plugins');
  1428. }
  1429. self::fireEvent('Atomik::Plugin::Before', array(&$plugin, &$config, &$options));
  1430. // checks if $plugin has been set to false from one of the event callbacks
  1431. if ($plugin === false) {
  1432. return false;
  1433. }
  1434. // tries to load the plugin from a file
  1435. if (($filename = self::path($plugin . '.php', $options['dirs'])) === false) {
  1436. // no file, checks for a directory
  1437. if (($dirname = self::path($plugin, $options['dirs'])) === false) {
  1438. // plugin not found
  1439. throw new Atomik_Exception('Missing plugin (no file or directory matching plugin name): ' . $plugin);
  1440. }
  1441. // directory found, plugin file should be inside
  1442. $filename = $dirname . '/Plugin.php';
  1443. $appFilename = $dirname . '/Application.php';
  1444. $pluginDir = $dirname;
  1445. if (!($isPluggApp = file_exists($appFilename)) && !file_exists($filename)) {
  1446. throw new Atomik_Exception('Missing plugin (no file inside the plugin\'s directory): ' . $plugin);
  1447. }
  1448. // registers the plugin as an application if Application.php exists
  1449. if ($isPluggApp && !isset(self::$pluggableApplications[$plugin])) {
  1450. self::registerPluggableApplication($plugin);
  1451. }
  1452. // adds the libraries folder from the plugin directory to the include path
  1453. if (@is_dir($dirname . '/libraries')) {
  1454. set_include_path($dirname . '/libraries'. PATH_SEPARATOR . get_include_path());
  1455. }
  1456. } else {
  1457. $pluginDir = dirname($filename);
  1458. }
  1459. // loads the plugin
  1460. self::log('Loading plugin ' . $plugin, LOG_DEBUG);
  1461. self::executeFile($filename, array('config' => $config));
  1462. // checks if the *Plugin class is defined. The use of this class
  1463. // is not mandatory in plugin file
  1464. $pluginClass = str_replace('%', $plugin, $options['classNameTemplate']);
  1465. if (class_exists($pluginClass, false)) {
  1466. $registerEventsCallback = true;
  1467. // call the start method on the plugin class if it's defined
  1468. if ($options['callStart'] && method_exists($pluginClass, 'start')) {
  1469. if (call_user_func(array($pluginClass, 'start'), $config) === false) {
  1470. $registerEventsCallback = false;
  1471. }
  1472. }
  1473. // automatically registers events callback for methods starting with "on"
  1474. if ($registerEventsCallback) {
  1475. self::attachClassListeners($pluginClass);
  1476. }
  1477. }
  1478. self::fireEvent('Atomik::Plugin::After', array($plugin));
  1479. // stores the plugin name so we won't load it twice
  1480. // also stores the directory from where it was loaded
  1481. self::$plugins[$plugin] = isset($pluginDir) ? rtrim($pluginDir, DIRECTORY_SEPARATOR) : true;
  1482. return true;
  1483. }
  1484. /**
  1485. * Loads a plugin only if it's available
  1486. *
  1487. * @see Atomik::loadPlugin()
  1488. */
  1489. public static function loadPluginIfAvailable($plugin)
  1490. {
  1491. if (!self::isPluginLoaded($plugin) && self::isPluginAvailable($plugin)) {
  1492. self::loadPlugin($plugin);
  1493. }
  1494. }
  1495. /**
  1496. * Loads a custom plugin only if it's available
  1497. *
  1498. * @see Atomik::loadPlugin()
  1499. */
  1500. public static function loadCustomPluginIfAvailable($plugin, $config = array(), $options = array())
  1501. {
  1502. if (!self::isPluginLoaded($plugin) && self::isPluginAvailable($plugin)) {
  1503. self::loadCustomPlugin($plugin, $config, $options);
  1504. }
  1505. }
  1506. /**
  1507. * Checks if a plugin is already loaded
  1508. *
  1509. * @param string $plugin
  1510. * @return bool
  1511. */
  1512. public static function isPluginLoaded($plugin)
  1513. {
  1514. return isset(self::$plugins[ucfirst($plugin)]);
  1515. }
  1516. /**
  1517. * Checks if a plugin is available
  1518. *
  1519. * @param string $plugin
  1520. * @return bool
  1521. */
  1522. public static function isPluginAvailable($plugin)
  1523. {
  1524. if (self::path($plugin . '.php', self::get('atomik/dirs/plugins')) === false) {
  1525. return self::path($plugin, self::get('atomik/dirs/plugins')) !== false;
  1526. }
  1527. return true;
  1528. }
  1529. /**
  1530. * Returns all loaded plugins
  1531. *
  1532. * @param bool $withDir Whether to only returns plugin names or the name (as array key) and the directory
  1533. * @return array
  1534. */
  1535. public static function getLoadedPlugins($withDir = false)
  1536. {
  1537. if ($withDir) {
  1538. return self::$plugins;
  1539. }
  1540. return array_keys(self::$plugins);
  1541. }
  1542. /**
  1543. * Registers a pluggable application
  1544. *
  1545. * Possible configuration keys are:
  1546. * - rootDir: directory inside the plugin directory where the application is stored (default empty string)
  1547. * - pluginDir: the plugin's directory (default to null, will find the directory automatically)
  1548. * - overwriteDirs: whether to keep access to the user actions, views, layouts and helpers folders
  1549. * - checkPluginIsLoaded: whether to check if the plugin is loaded
  1550. *
  1551. * @param string $plugin Plugin's name
  1552. * @param string $route The route that will trigger the application (default is the plugin name)
  1553. * @param array $config Configuration
  1554. */
  1555. public static function registerPluggableApplication($plugin, $route = null, $config = array())
  1556. {
  1557. $plugin = ucfirst($plugin);
  1558. self::fireEvent('Atomik::Registerpluggableapplication', array(&$plugin, &$route, &$config));
  1559. if (empty($plugin)) {
  1560. return;
  1561. }
  1562. // route
  1563. if ($route === null) {
  1564. // default route
  1565. $route = strtolower($plugin) . '/*';
  1566. }
  1567. self::$pluggableApplications[$plugin] = array(
  1568. 'plugin' => $plugin,
  1569. 'route' => trim($route, '/'),
  1570. 'config' => $config
  1571. );
  1572. }
  1573. /**
  1574. * Dispatches a pluggable application
  1575. *
  1576. * @see Atomik::registerPluggableApplication()
  1577. * @param string $plugin Plugin's name
  1578. * @param string $uri Uri
  1579. * @param array $config Configuration
  1580. * @return bool Dispatch success
  1581. */
  1582. public static function dispatchPluggableApplication($plugin, $uri = null, $config = array())
  1583. {
  1584. $plugin = ucfirst($plugin);
  1585. // configuration
  1586. $defaultConfig = array(
  1587. 'rootDir' => '',
  1588. 'pluginDir' => null,
  1589. 'resetConfig' => true,
  1590. 'overwriteDirs' => true,
  1591. 'checkPluginIsLoaded' => true
  1592. );
  1593. $config = array_merge($defaultConfig, $config);
  1594. if ($config['checkPluginIsLoaded'] && !self::isPluginLoaded($plugin)) {
  1595. return false;
  1596. }
  1597. // params check
  1598. if (empty($uri)) {
  1599. $uri = '';
  1600. }
  1601. $rootDir = rtrim('/' . trim($config['rootDir'], '/'), '/');
  1602. // plugin dir
  1603. if ($config['pluginDir'] === null) {
  1604. $pluginDir = self::$plugins[$plugin];
  1605. } else {
  1606. $pluginDir = rtrim($config['pluginDir'], '/');
  1607. }
  1608. // application dir
  1609. $appDir = $pluginDir . $rootDir;
  1610. if (!is_dir($appDir)) {
  1611. throw new Atomik_Exception('To be used as an application, the plugin ' . $plugin . ' must use a directory');
  1612. }
  1613. // overrides dir
  1614. $overrideDir = self::path($plugin . $rootDir, self::get('atomik/dirs/overrides'));
  1615. if ($overrideDir === false) {
  1616. $overrideDir = './app/overrides/' . $plugin . $rootDir;
  1617. } else {
  1618. $overrideDir = rtrim($overrideDir, '/');
  1619. }
  1620. if (!self::has('app/running_plugin')) {
  1621. // saves user configuration
  1622. self::set('userapp', self::get('app'));
  1623. }
  1624. // resets the configuration but keep the layout
  1625. if ($config['resetConfig']) {
  1626. $layout = self::get('app/layout');
  1627. self::reset();
  1628. self::set('app/layout', $layout);
  1629. self::set('app/routes', array());
  1630. }
  1631. self::set('app/running_plugin', $plugin);
  1632. // rewrite dirs
  1633. $dirs = array(
  1634. 'actions' => array($overrideDir . '/actions', $appDir . '/actions'),
  1635. 'views' => array($overrideDir . '/views', $appDir . '/views'),
  1636. 'layouts' => array($overrideDir . '/layouts', $overrideDir . '/views', $appDir . '/layouts', $appDir . '/views'),
  1637. 'helpers' => array($overrideDir . '/helpers', $appDir . '/helpers')
  1638. );
  1639. if ($config['overwriteDirs']) {
  1640. $dirs = array_merge(self::get('atomik/dirs'), $dirs);
  1641. } else {
  1642. $dirs = array_merge_recursive($dirs, self::get('atomik/dirs'));
  1643. }
  1644. self::set('atomik/dirs', $dirs);
  1645. // rewrite files
  1646. $files = self::get('atomik/files');
  1647. $files['pre_dispatch'] = $appDir . '/pre_dispatch.php';
  1648. $files['post_dispatch'] = $appDir . '/post_dispatch.php';
  1649. self::set('atomik/files', $files);
  1650. $cancel = false;
  1651. self::fireEvent('Atomik::Dispatchpluginapplication::Ready', array($plugin, &$uri, $config, &$cancel));
  1652. if ($cancel) {
  1653. return true;
  1654. }
  1655. // set the uri before including Application.php
  1656. self::set('request_uri', $uri);
  1657. // includes the Application.php file, equivalent to bootstrap.php for pluggable applications
  1658. $applicationFile = $appDir . '/Application.php';
  1659. if (file_exists($applicationFile)) {
  1660. $continue = include $applicationFile;
  1661. if ($continue === false) {
  1662. return true;
  1663. }
  1664. }
  1665. $cancel = false;
  1666. self::fireEvent('Atomik::Dispatchpluginapplication::Start', array($plugin, &$uri, $config, &$cancel));
  1667. if ($cancel) {
  1668. return true;
  1669. }
  1670. self::log('Dispatching pluggable application: ' . $plugin, LOG_DEBUG);
  1671. // re-dispatches the application
  1672. return self::dispatch($uri, false);
  1673. }
  1674. /**
  1675. * Registers a method that will be available on the Atomik class when using PHP5.3
  1676. * or through Atomik::call() for previous versions.
  1677. *
  1678. * @param string $method The method name
  1679. * @param callback $callback A callback to be called when the method is called
  1680. */
  1681. public static function registerMethod($method, $callback)
  1682. {
  1683. if (!is_callable($callback)) {
  1684. throw new Atomik_Exception('The specified callback for the method ' . $method . ' is not callable');
  1685. }
  1686. self::$methods[$method] = $callback;
  1687. }
  1688. /**
  1689. * Calls a registered method
  1690. *
  1691. * @param string $method Method name
  1692. * @param args $arg... Any number of arguments that will be pass to the method
  1693. * @return mixed The method result
  1694. */
  1695. public static function call($method)
  1696. {
  1697. if (!isset(self::$methods[$method])) {
  1698. throw new Atomik_Exception('Atomik::' . $method . '() not found');
  1699. }
  1700. $args = func_get_args();
  1701. array_shift($args);
  1702. return call_user_func_array(self::$methods[$method], $args);
  1703. }
  1704. /**
  1705. * PHP 5.3 magic method to handle calls to undefined method
  1706. */
  1707. public static function __callStatic($method, $args)
  1708. {
  1709. array_unshift($args, $method);
  1710. return call_user_func_array(array(self, 'callPlugin'), $args);
  1711. }
  1712. /* -------------------------------------------------------------------------------------------
  1713. * Events
  1714. * ------------------------------------------------------------------------------------------ */
  1715. /**
  1716. * Registers a callback to an event
  1717. *
  1718. * @param string $event Event name
  1719. * @param callback $callback The callback to call when the event is fired
  1720. * @param int $priority Listener priority
  1721. * @param bool $important If true and a listener of the same priority already exists, registers the new listener before the existing one.
  1722. */
  1723. public static function listenEvent($event, $callback, $priority = 50, $important = false)
  1724. {
  1725. // initialize the current event array */
  1726. if (!isset(self::$events[$event])) {
  1727. self::$events[$event] = array();
  1728. }
  1729. // while there is an event with the same priority, checks
  1730. // with an higher or lower priority
  1731. while (isset(self::$events[$event][$priority])) {
  1732. $priority += $important ? -1 : 1;
  1733. }
  1734. // stores the callback
  1735. self::$events[$event][$priority] = $callback;
  1736. }
  1737. /**
  1738. * Fires an event
  1739. *
  1740. * @param string $event The event name
  1741. * @param array $args Arguments for listeners
  1742. * @param bool $resultAsString Whether to return all listener results as a string
  1743. * @return array An array containing results of each executed listeners
  1744. */
  1745. public static function fireEvent($event, $args = array(), $resultAsString = false)
  1746. {
  1747. $results = array();
  1748. // executes all callback
  1749. if (isset(self::$events[$event])) {
  1750. $keys = array_keys(self::$events[$event]);
  1751. sort($keys);
  1752. foreach ($keys as $key) {
  1753. $callback = self::$events[$event][$key];
  1754. $results[$key] = call_user_func_array($callback, $args);
  1755. }
  1756. }
  1757. if ($resultAsString) {
  1758. return implode('', $results);
  1759. }
  1760. return $results;
  1761. }
  1762. /**
  1763. * Automatically registers event listeners for methods starting with "on"
  1764. *
  1765. * @param string|object $class
  1766. */
  1767. public static function attachClassListeners($class)
  1768. {
  1769. $methods = get_class_methods($class);
  1770. foreach ($methods as $method) {
  1771. if (preg_match('/^on[A-Z].*$/', $method)) {
  1772. $event = preg_replace('/(?<=\\w)([A-Z])/', '::\1', substr($method, 2));
  1773. self::listenEvent($event, array($class, $method));
  1774. }
  1775. }
  1776. }
  1777. /* -------------------------------------------------------------------------------------------
  1778. * Utilities
  1779. * ------------------------------------------------------------------------------------------ */
  1780. /**
  1781. * Builds a path.
  1782. *
  1783. * Multiple cases possible:
  1784. *
  1785. * 1) path($setOfPaths, $asArray = false)
  1786. * Returns a path from a set of paths (the set can be a string
  1787. * or an array). If the second argument is true, it returns
  1788. * all paths from the set as an array
  1789. *
  1790. * 2) path($file, $setOfPaths, $check = true)
  1791. * Searches for a file in the set of paths and returns the
  1792. * first one it finds. If $check is set to false, it returns
  1793. * the filename as if it was in the first path from the set of
  1794. * paths. Returns false when $check is true and no file where found.
  1795. *
  1796. * @param string|array $file
  1797. * @param string|array|bool $paths
  1798. * @param bool $check
  1799. * @return string|array
  1800. */
  1801. public static function path($file, $paths = null, $check = true)
  1802. {
  1803. // case1, $file is an array
  1804. if (is_array($file)) {
  1805. if ($paths === true) {
  1806. // returns $paths as array
  1807. return $file;
  1808. }
  1809. // returns the first path from the paths
  1810. return $file[0];
  1811. }
  1812. // $file is a string
  1813. // case 1
  1814. if ($paths === null || is_bool($paths)) {
  1815. if ($paths === true) {
  1816. // returns $file as array
  1817. return array($file);
  1818. }
  1819. // returns $file as string
  1820. return $file;
  1821. }
  1822. // case 2, $paths is a string
  1823. if (is_string($paths)) {
  1824. $filename = rtrim($paths, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $file;
  1825. if (!$check || file_exists($filename)) {
  1826. return $filename;
  1827. }
  1828. return false;
  1829. }
  1830. // case 2, $paths is an array
  1831. if (is_array($paths)) {
  1832. foreach ($paths as $path) {
  1833. $filename = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $file;
  1834. if (!$check || file_exists($filename)) {
  1835. return $filename;
  1836. }
  1837. }
  1838. }
  1839. // nothing found
  1840. return false;
  1841. }
  1842. /**
  1843. * Returns an action's filename
  1844. *
  1845. * @see Atomik::path()
  1846. * @param string $action Action name
  1847. * @param array $dirs Directories where actions are stored (default is using configuration)
  1848. * @return string
  1849. */
  1850. public static function actionFilename($action, $dirs = null)
  1851. {
  1852. if ($dirs === null) {
  1853. $dirs = self::get('atomik/dirs/actions');
  1854. }
  1855. if (($filename = self::path($action . '.php', $dirs)) === false) {
  1856. return self::path($action . '/index.php', $dirs);
  1857. }
  1858. return $filename;
  1859. }
  1860. /**
  1861. * Returns a view's filename
  1862. *
  1863. * @see Atomik::path()
  1864. * @param string $view View name
  1865. * @param array $dirs Directories where views are stored (default is using configuration)
  1866. * @param string $extension View's file extension
  1867. * @return string
  1868. */
  1869. public static function viewFilename($view, $dirs = null, $extension = null)
  1870. {
  1871. if ($dirs === null) {
  1872. $dirs = self::get('atomik/dirs/views');
  1873. }
  1874. if ($extension === null) {
  1875. $extension = ltrim(self::get('app/views/file_extension'), '.');
  1876. }
  1877. if (($filename = self::path($view . '.' . $extension, $dirs)) === false) {
  1878. return self::path($view . '/index.' . $extension, $dirs);
  1879. }
  1880. return $filename;
  1881. }
  1882. /**
  1883. * Returns an url for the action depending on whether url rewriting
  1884. * is used or not. Will return an url relative to the current application scope.
  1885. * Ie. if a plugin uses this method, it will return an url for an action of itself
  1886. *
  1887. * Can be used on links starting with a protocol but they will of course
  1888. * not be resolved like action names.
  1889. * Named routes can also be used (only specify the route name - with the @ - as the action)
  1890. *
  1891. * The item "_merge_GET" can be use in $params to merge GET parameters with specified params.
  1892. *
  1893. * @param string $action The action name or an url. Can contain GET parameters (after ?)
  1894. * @param array $params GET parameters to be added to the query string, if true will reuse current GET params
  1895. * @param bool $useIndex Whether to use index.php in the url
  1896. * @param bool $useBaseAction Whether to prepend the action with atomik/base_action
  1897. * @return string
  1898. */
  1899. public static function url($action = null, $params = array(), $useIndex = true, $useBaseAction = true)
  1900. {
  1901. $trigger = self::get('atomik/trigger', 'action');
  1902. if ($params === false) {
  1903. $params = array();
  1904. }
  1905. if ($params === true || in_array('__merge_GET', $params)) {
  1906. if (!is_array($params)) {
  1907. $params = array();
  1908. }
  1909. $GET = $_GET;
  1910. if (isset($GET[$trigger])) {
  1911. unset($GET[$trigger]);
  1912. }
  1913. if (($i = array_search('__merge_GET', $params)) !== false) {
  1914. unset($params[$i]);
  1915. }
  1916. $params = self::_mergeRecursive($GET, $params);
  1917. }
  1918. if ($action === null) {
  1919. $action = self::get('request_uri');
  1920. }
  1921. // removes the query string from the action
  1922. if (($separator = strpos($action, '?')) !== false) {
  1923. $queryString = parse_url($action, PHP_URL_QUERY);
  1924. $action = substr($action, 0, $separator);
  1925. parse_str($queryString, $actionParams);
  1926. $params = self::_mergeRecursive($actionParams, $params);
  1927. }
  1928. // checks if it's a named route
  1929. if ($action{0} == '@') {
  1930. $routeName = substr($action, 1);
  1931. $action = null;
  1932. foreach (self::get('app/routes') as $route => $default) {
  1933. if (!is_array($default) || !isset($default['@name']) ||
  1934. $default['@name'] != $routeName) {
  1935. continue;
  1936. }
  1937. if ($route{0} == '#') {
  1938. throw new Atomik_Exception('Named route (' . $routeName .
  1939. ') cannot use regular expressions');
  1940. }
  1941. $action = $route;
  1942. break;
  1943. }
  1944. if ($action === null) {
  1945. throw new Atomik_Exception('Missing route named ' . $routeName);
  1946. }
  1947. }
  1948. // injects parameters into the url
  1949. if (preg_match_all('/(:([a-zA-Z0-9_]+))/', $action, $matches)) {
  1950. for ($i = 0, $c = count($matches[0]); $i < $c; $i++) {
  1951. if (array_key_exists($matches[2][$i], $params)) {
  1952. $action = str_replace($matches[1][$i], $params[$matches[2][$i]], $action);
  1953. unset($params[$matches[2][$i]]);
  1954. }
  1955. }
  1956. }
  1957. // checks if $action is not a url (checking if there is a protocol)
  1958. if (!preg_match('/^([a-z]+):\/\/.*/', $action)) {
  1959. $action = ltrim($action, '/');
  1960. $url = rtrim(self::get('atomik/base_url', '.'), '/') . '/';
  1961. if ($useBaseAction) {
  1962. $action = ltrim(trim(self::get('atomik/base_action', ''), '/') . '/' . $action, '/');
  1963. }
  1964. // checks if url rewriting is used
  1965. if (!$useIndex || self::get('atomik/url_rewriting', false) == true) {
  1966. $useIndex = false;
  1967. $url .= $action;
  1968. } else {
  1969. // no url rewriting, using index.php
  1970. $url .= self::get('atomik/files/index', 'index.php');
  1971. $url .= '?' . $trigger . '=' . $action;
  1972. }
  1973. } else {
  1974. $useIndex = false;
  1975. $url = $action;
  1976. }
  1977. if (count($params)) {
  1978. $url .= ($useIndex ? '&amp;' : '?') . http_build_query($params, '', '&amp;');
  1979. }
  1980. // trigger an event
  1981. $args = func_get_args();
  1982. unset($args[0]);
  1983. self::fireEvent('Atomik::Url', array($action, &$url, $args));
  1984. return $url;
  1985. }
  1986. /**
  1987. * Returns an url exactly like Atomik::url() but relative to the root application.
  1988. *
  1989. * @see Atomik::url()
  1990. * @param string $action
  1991. * @param array $params
  1992. * @param bool $useIndex
  1993. * @return string
  1994. */
  1995. public static function appUrl($action, $params = array(), $useIndex = true)
  1996. {
  1997. return self::url($action, $params, $useIndex, false);
  1998. }
  1999. /**
  2000. * Returns an url exactly like Atomik::url() but relative to a plugin
  2001. *
  2002. * @see Atomik::url()
  2003. * @param string $plugin The name of a plugin which is used as a pluggable application
  2004. * @param string $action
  2005. * @param array $params
  2006. * @param bool $useIndex
  2007. * @return string
  2008. */
  2009. public static function pluginUrl($plugin, $action, $params = array(), $useIndex = true)
  2010. {
  2011. $plugin = ucfirst($plugin);
  2012. if (!isset(self::$pluggableApplications[$plugin])) {
  2013. throw new Atomik_Exception('Plugin ' . $plugin . ' is not registered as a pluggable application');
  2014. }
  2015. $route = rtrim(self::$pluggableApplications[$plugin]['route'], '/*');
  2016. return self::url($route . '/' . ltrim($action, '/'), $params, $useIndex, false);
  2017. }
  2018. /**
  2019. * Returns the url of an asset file (ie. an url without index.php) relative
  2020. * to the current application scope
  2021. *
  2022. * @see Atomik::url()
  2023. * @param string $filename
  2024. * @param array $params
  2025. * @return string
  2026. */
  2027. public static function asset($filename, $params = array())
  2028. {
  2029. if (self::has('app/running_plugin')) {
  2030. if (($plugin = self::get('app/running_plugin')) === null) {
  2031. throw new Atomik_Exception('Invalid running plugin name');
  2032. }
  2033. return self::pluginAsset($plugin, $filename, $params);
  2034. }
  2035. return self::appAsset($filename, $params);
  2036. }
  2037. /**
  2038. * Returns the url of an asset file relative to the root application
  2039. *
  2040. * @see Atomik::asset()
  2041. * @param string $filename
  2042. * @param array $params
  2043. * @return string
  2044. */
  2045. public static function appAsset($filename, $params = array())
  2046. {
  2047. return self::url($filename, $params, false, false);
  2048. }
  2049. /**
  2050. * Returns the url of a plugin's asset file following the path template
  2051. * defined in the configuration.
  2052. *
  2053. * @see Atomik::url()
  2054. * @param string $plugin Plugin's name (default is the currently running pluggable app)
  2055. * @param string $filename
  2056. * @param array $params
  2057. * @return string
  2058. */
  2059. public static function pluginAsset($plugin, $filename, $params = array())
  2060. {
  2061. $template = self::get('atomik/plugin_assets_tpl', 'app/plugins/%s/assets');
  2062. $dirname = rtrim(sprintf($template, ucfirst($plugin)), '/');
  2063. $filename = '/' . ltrim($filename, '/');
  2064. return self::url($dirname . $filename, $params, false, false);
  2065. }
  2066. /*
  2067. * Includes a file
  2068. *
  2069. * @param string $include Filename or class name following the PEAR convention
  2070. * @param bool $className If true, $include will be transformed from a PEAR-formed class name to a filename
  2071. * @param string|array $dirs Include from specific directories rather than include path
  2072. * @return bool
  2073. */
  2074. public static function needed($include, $className = true, $dirs = null)
  2075. {
  2076. self::fireEvent('Atomik::Needed', array(&$include, &$className, &$dirs));
  2077. if ($include === null) {
  2078. return;
  2079. }
  2080. if ($dirs === null) {
  2081. $dirs = explode(PATH_SEPARATOR, get_include_path());
  2082. }
  2083. if ($className && strpos($include, '_') !== false) {
  2084. $include = str_replace('_', DIRECTORY_SEPARATOR, $include);
  2085. }
  2086. $include .= '.php';
  2087. if (($filename = self::path($include, $dirs)) === false) {
  2088. return false;
  2089. }
  2090. return include_once($filename);
  2091. }
  2092. /**
  2093. * Class autoloader
  2094. *
  2095. * @see spl_autoload_register()
  2096. * @param string $className
  2097. * @return bool
  2098. */
  2099. public static function autoload($className)
  2100. {
  2101. try {
  2102. self::needed($className);
  2103. return true;
  2104. } catch (Exception $e) {
  2105. return false;
  2106. }
  2107. }
  2108. /**
  2109. * Escapes text so it can be outputted safely
  2110. *
  2111. * Uses escape profiles defined in the escaping configuration key
  2112. *
  2113. * @param string $text The text to escape
  2114. * @param mixed $profile A profile name, a function name, or an array of function
  2115. * @return string The escaped string
  2116. */
  2117. public static function escape($text, $profile = 'default')
  2118. {
  2119. if (!is_array($profile)) {
  2120. if (($functions = self::get('app/escaping/' . $profile, false)) === false) {
  2121. if (function_exists($profile)) {
  2122. $functions = array($profile);
  2123. } else {
  2124. $functions = array('htmlspecialchars');
  2125. }
  2126. }
  2127. } else {
  2128. $functions = $profile;
  2129. }
  2130. foreach ($functions as $function) {
  2131. $text = call_user_func($function, $text);
  2132. }
  2133. return $text;
  2134. }
  2135. /**
  2136. * Saves a message that can be retrieve only once
  2137. *
  2138. * @param string|array $message One message as a string or many messages as an array
  2139. * @param string $label
  2140. */
  2141. public static function flash($message, $label = 'default')
  2142. {
  2143. if (!isset($_SESSION)) {
  2144. throw new Atomik_Exception('The session must be started before using Atomik::flash()');
  2145. }
  2146. self::fireEvent('Atomik::Flash', array(&$message, &$label));
  2147. if (!self::has('session/__FLASH/' . $label)) {
  2148. self::set('session/__FLASH/' . $label, array());
  2149. }
  2150. self::add('session/__FLASH/' . $label, $message);
  2151. }
  2152. /**
  2153. * Returns the flash messages saved in the session
  2154. *
  2155. * @internal
  2156. * @param string $label Whether to only retreives messages from this label. When null or 'all', returns all messages
  2157. * @param bool $delete Whether to delete messages once retrieved
  2158. * @return array An array of messages if the label is specified or an array of array message
  2159. */
  2160. public static function _getFlashMessages($label = 'all', $delete = true) {
  2161. if (!isset($_SESSION['__FLASH'])) {
  2162. return array();
  2163. }
  2164. if (empty($label) || $label == 'all') {
  2165. if ($delete) {
  2166. return self::delete('session/__FLASH');
  2167. }
  2168. return self::get('session/__FLASH');
  2169. }
  2170. if (!isset($_SESSION['__FLASH'][$label])) {
  2171. return array();
  2172. }
  2173. if ($delete) {
  2174. return self::delete('session/__FLASH/' . $label);
  2175. }
  2176. return self::get('session/__FLASH/' . $label);
  2177. }
  2178. /**
  2179. * Filters data using PHP's filter extension
  2180. *
  2181. * @see filter_var()
  2182. * @param mixed $data
  2183. * @param mixed $filter
  2184. * @param mixed $options
  2185. * @param bool $falseOnFail
  2186. * @return mixed
  2187. */
  2188. public static function filter($data, $filter = null, $options = null, $falseOnFail = true)
  2189. {
  2190. if (is_array($data)) {
  2191. // the $filter must be a rule or a string to a rule defined under app/filters/rules
  2192. if (is_string($filter)) {
  2193. if (($rule = self::get('app/filters/rules/' . $filter, false)) === false) {
  2194. throw new Atomik_Exception('When $data is an array, the filter must be an array of definition or a rule name in Atomik::filter()');
  2195. }
  2196. } else {
  2197. $rule = $filter;
  2198. }
  2199. $results = array();
  2200. $messages = array();
  2201. $validate = true;
  2202. foreach ($rule as $field => $params) {
  2203. if (isset($data[$field]) && is_array($data[$field])) {
  2204. // data is an array
  2205. if (($results[$field] = self::filter($data[$field], $params)) === false) {
  2206. $messages[$field] = A('app/filters/messages', array());
  2207. $validate = false;
  2208. }
  2209. continue;
  2210. }
  2211. $filter = FILTER_SANITIZE_STRING;
  2212. $message = self::get('app/filters/default_message', 'The %s field failed to validate');
  2213. $required = false;
  2214. $default = null;
  2215. $label = $field;
  2216. if (is_array($params)) {
  2217. // extracting information from the array
  2218. if (isset($params['message'])) {
  2219. $message = self::delete('message', $params);
  2220. }
  2221. if (isset($params['required'])) {
  2222. $required = self::delete('required', $params);
  2223. }
  2224. if (isset($params['default'])) {
  2225. $default = self::delete('default', $params);
  2226. }
  2227. if (isset($params['label'])) {
  2228. $label = self::delete('label', $params);
  2229. }
  2230. if (isset($params['filter'])) {
  2231. $filter = self::delete('filter', $params);
  2232. }
  2233. $options = count($params) == 0 ? null : $params;
  2234. } else {
  2235. $filter = $params;
  2236. $options = null;
  2237. }
  2238. if (!isset($data[$field]) && !$required) {
  2239. // field not set and not required, do nothing
  2240. continue;
  2241. }
  2242. if ((!isset($data[$field]) || $data[$field] == '') && $required) {
  2243. // the field is required and either not set or empty, this is an error
  2244. $results[$field] = false;
  2245. $message = self::get('app/filters/required_message', 'The %s field must be filled');
  2246. } else if ($data[$field] === '' && !$required) {
  2247. // empty but not required, null value
  2248. $results[$field] = $default;
  2249. } else {
  2250. // normal, validating
  2251. $results[$field] = self::filter($data[$field], $filter, $options);
  2252. }
  2253. if ($results[$field] === false) {
  2254. // failed validation, adding the message
  2255. $messages[$field] = sprintf($message, $label);
  2256. $validate = false;
  2257. }
  2258. }
  2259. self::set('app/filters/messages', $messages);
  2260. return $validate || !$falseOnFail ? $results : false;
  2261. }
  2262. if (is_string($filter)) {
  2263. if (in_array($filter, filter_list())) {
  2264. // filter name from the extension filters
  2265. $filter = filter_id($filter);
  2266. } else if (preg_match('@/.+/[a-zA-Z]*@', $filter)) {
  2267. // regexp
  2268. $options = array('options' => array('regexp' => $filter));
  2269. $filter = FILTER_VALIDATE_REGEXP;
  2270. } else if (($callback = self::get('app/filters/callbacks/' . $filter, false)) !== false) {
  2271. // callback defined under app/filters/callbacks
  2272. $filter = FILTER_CALLBACK;
  2273. $options = $callback;
  2274. }
  2275. }
  2276. return filter_var($data, $filter, $options);
  2277. }
  2278. /**
  2279. * Makes a string friendly to urls
  2280. *
  2281. * @param string $string
  2282. * @return string
  2283. */
  2284. public static function friendlify($string)
  2285. {
  2286. $string = str_replace('-', ' ', $string);
  2287. $string = preg_replace(array('/\s+/', '/[^A-Za-z0-9\-]/'), array('-', ''), $string);
  2288. $string = trim(strtolower($string));
  2289. return $string;
  2290. }
  2291. /**
  2292. * Redirects to another url
  2293. *
  2294. * @see Atomik::url()
  2295. * @param string $url The url to redirect to
  2296. * @param bool $useUrl Whether to use Atomik::url() on $url before redirecting
  2297. * @param int $httpCode The redirection HTTP code
  2298. */
  2299. public static function redirect($url, $useUrl = true, $httpCode = 302)
  2300. {
  2301. self::fireEvent('Atomik::Redirect', array(&$url, &$useUrl, &$httpCode));
  2302. if ($url === false) {
  2303. return;
  2304. }
  2305. // uses Atomik::url()
  2306. if ($useUrl) {
  2307. $url = self::url($url);
  2308. }
  2309. if (isset($_SESSION)) {
  2310. $session = $_SESSION;
  2311. // seems to prevent a php bug with session before redirections
  2312. session_regenerate_id(true);
  2313. $_SESSION = $session;
  2314. // avoid loosing the session
  2315. session_write_close();
  2316. }
  2317. // redirects
  2318. header('Location: ' . $url, true, $httpCode);
  2319. self::end(true, false);
  2320. }
  2321. /**
  2322. * Same as Atomik::redirect() but for urls relative to the root application
  2323. *
  2324. * @see Atomik::redirect()
  2325. * @param string $url The url to redirect to
  2326. * @param bool $useUrl Use Atomik::url() on $url before redirecting
  2327. * @param int $httpCode The redirection HTTP code
  2328. */
  2329. public static function appRedirect($url, $useUrl = true, $httpCode = 302)
  2330. {
  2331. // uses Atomik::appUrl()
  2332. if ($useUrl) {
  2333. $url = self::appUrl($url);
  2334. }
  2335. self::redirect($url, false, $httpCode);
  2336. }
  2337. /**
  2338. * Same as Atomik::redirect() but redirects to a pluggable application
  2339. *
  2340. * @see Atomik::redirect()
  2341. * @param string $url The url to redirect to
  2342. * @param bool $useUrl Use Atomik::url() on $url before redirecting
  2343. * @param int $httpCode The redirection HTTP code
  2344. */
  2345. public static function pluginRedirect($plugin, $url, $useUrl = true, $httpCode = 302)
  2346. {
  2347. // uses Atomik::pluginUrl()
  2348. if ($useUrl) {
  2349. $url = self::pluginUrl($plugin, $url);
  2350. }
  2351. self::redirect($url, false, $httpCode);
  2352. }
  2353. /**
  2354. * Triggers a 404 error
  2355. */
  2356. public static function trigger404()
  2357. {
  2358. self::fireEvent('Atomik::404', array(&$cancel));
  2359. if ($cancel) {
  2360. return;
  2361. }
  2362. self::log('404 ERROR: ' . self::get('full_request_uri'), LOG_ERR);
  2363. // HTTP headers
  2364. header('HTTP/1.0 404 Not Found');
  2365. header('Content-type: text/html');
  2366. if (file_exists($filename = self::get('atomik/files/404'))) {
  2367. // includes the 404 error file
  2368. include($filename);
  2369. } else {
  2370. echo '<h1>404 - File not found</h1>';
  2371. }
  2372. self::end();
  2373. }
  2374. /**
  2375. * Fire an Atomik::Log event to which logger can listen
  2376. *
  2377. * @param string $message
  2378. * @param int $level
  2379. */
  2380. public static function log($message, $level = 3)
  2381. {
  2382. self::fireEvent('Atomik::Log', array($message, $level));
  2383. }
  2384. /**
  2385. * Default logger: log the message to the file defined in atomik/files/log
  2386. * The message template can be define in atomik/log/message_template
  2387. *
  2388. * @see Atomik::log()
  2389. * @param string $message
  2390. * @param int $level
  2391. */
  2392. public static function logToFile($message, $level)
  2393. {
  2394. if ($level > self::get('atomik/log/level')) {
  2395. return;
  2396. }
  2397. $filename = self::get('atomik/files/log');
  2398. $template = self::get('atomik/log/message_template', '[%date%] [%level%] %message%');
  2399. $tags = array(
  2400. '%date%' => @date('Y-m-d H:i:s'),
  2401. '%level%' => $level,
  2402. '%message%' => $message
  2403. );
  2404. $file = fopen($filename, 'a');
  2405. fwrite($file, str_replace(array_keys($tags), array_values($tags), $template) . "\n");
  2406. fclose($file);
  2407. $file = null;
  2408. }
  2409. /**
  2410. * Equivalent to var_dump() but can be disabled using the configuration
  2411. *
  2412. * @see var_dump()
  2413. * @param mixed $data The data which value should be dumped
  2414. * @param bool $force Always display the dump even if debug from the config is set to false
  2415. * @param bool $echo Whether to echo or return the result
  2416. * @return string The result or null if $echo is set to true
  2417. */
  2418. public static function debug($data, $force = false, $echo = true)
  2419. {
  2420. if (!$force && !self::get('atomik/debug', false)) {
  2421. return;
  2422. }
  2423. self::fireEvent('Atomik::Debug', array(&$data, &$force, &$echo));
  2424. // var_dump() does not support returns
  2425. ob_start();
  2426. var_dump($data);
  2427. $dump = ob_get_clean();
  2428. if (!$echo) {
  2429. return $dump;
  2430. }
  2431. echo $dump;
  2432. }
  2433. /**
  2434. * Catch errors and throw an ErrorException instead
  2435. *
  2436. * @internal
  2437. * @param int $errno
  2438. * @param string $errstr
  2439. * @param string $errfile
  2440. * @param int $errline
  2441. * @param mixed $errcontext
  2442. */
  2443. public static function _errorHandler($errno, $errstr, $errfile = '', $errline = 0, $errcontext = null)
  2444. {
  2445. // handles errors depending on the level defined with error_reporting
  2446. if ($errno <= error_reporting()) {
  2447. throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
  2448. }
  2449. }
  2450. /**
  2451. * Renders an exception
  2452. *
  2453. * @param Exception $exception The exception which sould ne rendered
  2454. * @param bool $return Return the output instead of printing it
  2455. * @return string
  2456. */
  2457. public static function renderException($exception, $return = false)
  2458. {
  2459. // checks if the user defined error file is available
  2460. if (file_exists($filename = self::get('atomik/files/error'))) {
  2461. include($filename);
  2462. self::end(false);
  2463. }
  2464. $attributes = self::get('atomik/error_report_attrs');
  2465. echo '<div ' . $attributes['atomik-error'] . '>'
  2466. . '<span ' . $attributes['atomik-error-title'] . '>'
  2467. . 'An error has occured!</span>';
  2468. // only display error information if atomik/display_errors is true
  2469. if (self::get('atomik/display_errors', false) === false) {
  2470. echo '</div>';
  2471. self::end(false);
  2472. }
  2473. // builds the html erro report
  2474. $html = '<br />An error of type <strong>' . get_class($exception) . '</strong> '
  2475. . 'was caught at <strong>line ' . $exception->getLine() . '</strong><br />'
  2476. . 'in file <strong>' . $exception->getFile() . '</strong>'
  2477. . '<p>' . $exception->getMessage() . '</p>'
  2478. . '<table ' . $attributes['atomik-error-lines'] . '>';
  2479. // builds the table which display the lines around the error
  2480. $lines = file($exception->getFile());
  2481. $start = $exception->getLine() - 7;
  2482. $start = $start < 0 ? 0 : $start;
  2483. $end = $exception->getLine() + 7;
  2484. $end = $end > count($lines) ? count($lines) : $end;
  2485. for($i = $start; $i < $end; $i++) {
  2486. // color the line with the error. with standard Exception, lines are
  2487. if($i == $exception->getLine() - (get_class($exception) != 'ErrorException' ? 1 : 0)) {
  2488. $html .= '<tr ' . $attributes['atomik-error-line-error'] . '><td>';
  2489. }
  2490. else {
  2491. $html .= '<tr ' . $attributes['atomik-error-line'] . '>'
  2492. . '<td ' . $attributes['atomik-error-line-number'] . '>';
  2493. }
  2494. $html .= $i . '</td><td ' . $attributes['atomik-error-line-text'] . '>'
  2495. . (isset($lines[$i]) ? htmlspecialchars($lines[$i]) : '') . '</td></tr>';
  2496. }
  2497. $html .= '</table>'
  2498. . '<strong>Stack:</strong><p ' . $attributes['atomik-error-stack'] . '>'
  2499. . nl2br($exception->getTraceAsString())
  2500. . '</p></div>';
  2501. echo $html;
  2502. }
  2503. }