diff options
Diffstat (limited to 'app/Core')
-rw-r--r-- | app/Core/Base.php | 112 | ||||
-rw-r--r-- | app/Core/Cache.php | 58 | ||||
-rw-r--r-- | app/Core/Cli.php | 75 | ||||
-rw-r--r-- | app/Core/EmailClient.php | 46 | ||||
-rw-r--r-- | app/Core/Event.php | 164 | ||||
-rw-r--r-- | app/Core/Helper.php | 60 | ||||
-rw-r--r-- | app/Core/HttpClient.php | 117 | ||||
-rw-r--r-- | app/Core/Listener.php | 21 | ||||
-rw-r--r-- | app/Core/Loader.php | 62 | ||||
-rw-r--r-- | app/Core/Markdown.php | 47 | ||||
-rw-r--r-- | app/Core/MemoryCache.php | 32 | ||||
-rw-r--r-- | app/Core/Paginator.php | 461 | ||||
-rw-r--r-- | app/Core/Registry.php | 83 | ||||
-rw-r--r-- | app/Core/Request.php | 25 | ||||
-rw-r--r-- | app/Core/Response.php | 38 | ||||
-rw-r--r-- | app/Core/Router.php | 24 | ||||
-rw-r--r-- | app/Core/Session.php | 64 | ||||
-rw-r--r-- | app/Core/Template.php | 14 | ||||
-rw-r--r-- | app/Core/Tool.php | 31 | ||||
-rw-r--r-- | app/Core/Translator.php | 34 |
20 files changed, 1077 insertions, 491 deletions
diff --git a/app/Core/Base.php b/app/Core/Base.php new file mode 100644 index 00000000..6cb87cbc --- /dev/null +++ b/app/Core/Base.php @@ -0,0 +1,112 @@ +<?php + +namespace Core; + +use Pimple\Container; + +/** + * Base class + * + * @package core + * @author Frederic Guillot + * + * @property \Core\Helper $helper + * @property \Core\EmailClient $emailClient + * @property \Core\HttpClient $httpClient + * @property \Core\Paginator $paginator + * @property \Core\Request $request + * @property \Core\Session $session + * @property \Core\Template $template + * @property \Integration\BitbucketWebhook $bitbucketWebhook + * @property \Integration\GithubWebhook $githubWebhook + * @property \Integration\GitlabWebhook $gitlabWebhook + * @property \Integration\HipchatWebhook $hipchatWebhook + * @property \Integration\Jabber $jabber + * @property \Integration\Mailgun $mailgun + * @property \Integration\Postmark $postmark + * @property \Integration\SendgridWebhook $sendgridWebhook + * @property \Integration\SlackWebhook $slackWebhook + * @property \Integration\Smtp $smtp + * @property \Model\Acl $acl + * @property \Model\Action $action + * @property \Model\Authentication $authentication + * @property \Model\Board $board + * @property \Model\Budget $budget + * @property \Model\Category $category + * @property \Model\Color $color + * @property \Model\Comment $comment + * @property \Model\Config $config + * @property \Model\Currency $currency + * @property \Model\DateParser $dateParser + * @property \Model\File $file + * @property \Model\HourlyRate $hourlyRate + * @property \Model\LastLogin $lastLogin + * @property \Model\Link $link + * @property \Model\Notification $notification + * @property \Model\Project $project + * @property \Model\ProjectActivity $projectActivity + * @property \Model\ProjectAnalytic $projectAnalytic + * @property \Model\ProjectDuplication $projectDuplication + * @property \Model\ProjectDailySummary $projectDailySummary + * @property \Model\ProjectIntegration $projectIntegration + * @property \Model\ProjectPermission $projectPermission + * @property \Model\Subtask $subtask + * @property \Model\SubtaskExport $subtaskExport + * @property \Model\SubtaskForecast $subtaskForecast + * @property \Model\SubtaskTimeTracking $subtaskTimeTracking + * @property \Model\Swimlane $swimlane + * @property \Model\Task $task + * @property \Model\TaskCreation $taskCreation + * @property \Model\TaskDuplication $taskDuplication + * @property \Model\TaskExport $taskExport + * @property \Model\TaskFinder $taskFinder + * @property \Model\TaskFilter $taskFilter + * @property \Model\TaskLink $taskLink + * @property \Model\TaskModification $taskModification + * @property \Model\TaskPermission $taskPermission + * @property \Model\TaskPosition $taskPosition + * @property \Model\TaskStatus $taskStatus + * @property \Model\TaskValidator $taskValidator + * @property \Model\Timetable $timetable + * @property \Model\TimetableDay $timetableDay + * @property \Model\TimetableExtra $timetableExtra + * @property \Model\TimetableOff $timetableOff + * @property \Model\TimetableWeek $timetableWeek + * @property \Model\Transition $transition + * @property \Model\User $user + * @property \Model\UserSession $userSession + * @property \Model\Webhook $webhook + */ +abstract class Base +{ + /** + * Container instance + * + * @access protected + * @var \Pimple\Container + */ + protected $container; + + /** + * Constructor + * + * @access public + * @param \Pimple\Container $container + */ + public function __construct(Container $container) + { + $this->container = $container; + } + + /** + * Load automatically models + * + * @access public + * @param string $name Model name + * @return mixed + */ + public function __get($name) + { + return $this->container[$name]; + } +} diff --git a/app/Core/Cache.php b/app/Core/Cache.php new file mode 100644 index 00000000..670a76e0 --- /dev/null +++ b/app/Core/Cache.php @@ -0,0 +1,58 @@ +<?php + +namespace Core; + +use Pimple\Container; + +abstract class Cache +{ + /** + * Container instance + * + * @access protected + * @var \Pimple\Container + */ + protected $container; + + abstract public function init(); + abstract public function set($key, $value); + abstract public function get($key); + abstract public function flush(); + abstract public function remove($key); + + /** + * Constructor + * + * @access public + * @param \Pimple\Container $container + */ + public function __construct(Container $container) + { + $this->container = $container; + $this->init(); + } + + /** + * Proxy cache + * + * Note: Arguments must be scalar types + * + * @access public + * @param string $container Container name + * @param string $method Container method + * @return mixed + */ + public function proxy($container, $method) + { + $args = func_get_args(); + $key = 'proxy_'.implode('_', $args); + $result = $this->get($key); + + if ($result === null) { + $result = call_user_func_array(array($this->container[$container], $method), array_splice($args, 2)); + $this->set($key, $result); + } + + return $result; + } +} diff --git a/app/Core/Cli.php b/app/Core/Cli.php deleted file mode 100644 index 13533b9a..00000000 --- a/app/Core/Cli.php +++ /dev/null @@ -1,75 +0,0 @@ -<?php - -namespace Core; - -use Closure; - -/** - * CLI class - * - * @package core - * @author Frederic Guillot - */ -class Cli -{ - /** - * Default command name - * - * @access public - * @var string - */ - public $default_command = 'help'; - - /** - * List of registered commands - * - * @access private - * @var array - */ - private $commands = array(); - - /** - * - * - * @access public - * @param string $command Command name - * @param Closure $callback Command callback - */ - public function register($command, Closure $callback) - { - $this->commands[$command] = $callback; - } - - /** - * Execute a command - * - * @access public - * @param string $command Command name - */ - public function call($command) - { - if (isset($this->commands[$command])) { - $this->commands[$command](); - exit; - } - } - - /** - * Determine which command to execute - * - * @access public - */ - public function execute() - { - if (php_sapi_name() !== 'cli') { - die('This script work only from the command line.'); - } - - if ($GLOBALS['argc'] === 1) { - $this->call($this->default_command); - } - - $this->call($GLOBALS['argv'][1]); - $this->call($this->default_command); - } -} diff --git a/app/Core/EmailClient.php b/app/Core/EmailClient.php new file mode 100644 index 00000000..07687c42 --- /dev/null +++ b/app/Core/EmailClient.php @@ -0,0 +1,46 @@ +<?php + +namespace Core; + +/** + * Mail client + * + * @package core + * @author Frederic Guillot + */ +class EmailClient extends Base +{ + /** + * Send a HTML email + * + * @access public + * @param string $email + * @param string $name + * @param string $subject + * @param string $html + */ + public function send($email, $name, $subject, $html) + { + $this->container['logger']->debug('Sending email to '.$email.' ('.MAIL_TRANSPORT.')'); + + $start_time = microtime(true); + $author = 'Kanboard'; + + if (Session::isOpen() && $this->userSession->isLogged()) { + $author = e('%s via Kanboard', $this->user->getFullname($this->session['user'])); + } + + switch (MAIL_TRANSPORT) { + case 'mailgun': + $this->mailgun->sendEmail($email, $name, $subject, $html, $author); + break; + case 'postmark': + $this->postmark->sendEmail($email, $name, $subject, $html, $author); + break; + default: + $this->smtp->sendEmail($email, $name, $subject, $html, $author); + } + + $this->container['logger']->debug('Email sent in '.round(microtime(true) - $start_time, 6).' seconds'); + } +} diff --git a/app/Core/Event.php b/app/Core/Event.php deleted file mode 100644 index a32499d8..00000000 --- a/app/Core/Event.php +++ /dev/null @@ -1,164 +0,0 @@ -<?php - -namespace Core; - -/** - * Event dispatcher class - * - * @package core - * @author Frederic Guillot - */ -class Event -{ - /** - * Contains all listeners - * - * @access private - * @var array - */ - private $listeners = array(); - - /** - * The last listener executed - * - * @access private - * @var string - */ - private $lastListener = ''; - - /** - * The last triggered event - * - * @access private - * @var string - */ - private $lastEvent = ''; - - /** - * Triggered events list - * - * @access private - * @var array - */ - private $events = array(); - - /** - * Attach a listener object to an event - * - * @access public - * @param string $eventName Event name - * @param Listener $listener Object that implements the Listener interface - */ - public function attach($eventName, Listener $listener) - { - if (! isset($this->listeners[$eventName])) { - $this->listeners[$eventName] = array(); - } - - $this->listeners[$eventName][] = $listener; - } - - /** - * Trigger an event - * - * @access public - * @param string $eventName Event name - * @param array $data Event data - */ - public function trigger($eventName, array $data) - { - if (! $this->isEventTriggered($eventName)) { - - $this->events[] = $eventName; - - if (isset($this->listeners[$eventName])) { - - foreach ($this->listeners[$eventName] as $listener) { - - $this->lastEvent = $eventName; - - if ($listener->execute($data)) { - $this->lastListener = get_class($listener); - } - } - } - } - } - - /** - * Get the last listener executed - * - * @access public - * @return string Event name - */ - public function getLastListenerExecuted() - { - return $this->lastListener; - } - - /** - * Get the last fired event - * - * @access public - * @return string Event name - */ - public function getLastTriggeredEvent() - { - return $this->lastEvent; - } - - /** - * Get a list of triggered events - * - * @access public - * @return array - */ - public function getTriggeredEvents() - { - return $this->events; - } - - /** - * Check if an event have been triggered - * - * @access public - * @param string $eventName Event name - * @return bool - */ - public function isEventTriggered($eventName) - { - return in_array($eventName, $this->events); - } - - /** - * Flush the list of triggered events - * - * @access public - */ - public function clearTriggeredEvents() - { - $this->events = array(); - $this->lastEvent = ''; - } - - /** - * Check if a listener bind to an event - * - * @access public - * @param string $eventName Event name - * @param mixed $instance Instance name or object itself - * @return bool Yes or no - */ - public function hasListener($eventName, $instance) - { - if (isset($this->listeners[$eventName])) { - foreach ($this->listeners[$eventName] as $listener) { - if ($listener instanceof $instance) { - return true; - } - } - } - - return false; - } -} diff --git a/app/Core/Helper.php b/app/Core/Helper.php new file mode 100644 index 00000000..53084a7e --- /dev/null +++ b/app/Core/Helper.php @@ -0,0 +1,60 @@ +<?php + +namespace Core; + +/** + * Helper base class + * + * @package core + * @author Frederic Guillot + * + * @property \Helper\App $app + * @property \Helper\Asset $asset + * @property \Helper\Datetime $datetime + * @property \Helper\File $file + * @property \Helper\Form $form + * @property \Helper\Subtask $subtask + * @property \Helper\Task $task + * @property \Helper\Text $text + * @property \Helper\Url $url + * @property \Helper\User $user + */ +class Helper extends Base +{ + /** + * Helper instances + * + * @static + * @access private + * @var array + */ + private static $helpers = array(); + + /** + * Load automatically helpers + * + * @access public + * @param string $name Helper name + * @return mixed + */ + public function __get($name) + { + if (! isset(self::$helpers[$name])) { + $class = '\Helper\\'.ucfirst($name); + self::$helpers[$name] = new $class($this->container); + } + + return self::$helpers[$name]; + } + + /** + * HTML escaping + * + * @param string $value Value to escape + * @return string + */ + public function e($value) + { + return htmlspecialchars($value, ENT_QUOTES, 'UTF-8', false); + } +} diff --git a/app/Core/HttpClient.php b/app/Core/HttpClient.php new file mode 100644 index 00000000..2f280a1e --- /dev/null +++ b/app/Core/HttpClient.php @@ -0,0 +1,117 @@ +<?php + +namespace Core; + +/** + * HTTP client + * + * @package core + * @author Frederic Guillot + */ +class HttpClient extends Base +{ + /** + * HTTP connection timeout in seconds + * + * @var integer + */ + const HTTP_TIMEOUT = 5; + + /** + * Number of maximum redirections for the HTTP client + * + * @var integer + */ + const HTTP_MAX_REDIRECTS = 2; + + /** + * HTTP client user agent + * + * @var string + */ + const HTTP_USER_AGENT = 'Kanboard'; + + /** + * Send a POST HTTP request encoded in JSON + * + * @access public + * @param string $url + * @param array $data + * @param array $headers + * @return string + */ + public function postJson($url, array $data, array $headers = array()) + { + return $this->doRequest( + $url, + json_encode($data), + array_merge(array('Content-type: application/json'), $headers) + ); + } + + /** + * Send a POST HTTP request encoded in www-form-urlencoded + * + * @access public + * @param string $url + * @param array $data + * @param array $headers + * @return string + */ + public function postForm($url, array $data, array $headers = array()) + { + return $this->doRequest( + $url, + http_build_query($data), + array_merge(array('Content-type: application/x-www-form-urlencoded'), $headers) + ); + } + + /** + * Make the HTTP request + * + * @access private + * @param string $url + * @param array $content + * @param array $headers + * @return string + */ + private function doRequest($url, $content, array $headers) + { + if (empty($url)) { + return ''; + } + + $headers = array_merge(array('User-Agent: '.self::HTTP_USER_AGENT, 'Connection: close'), $headers); + + $context = stream_context_create(array( + 'http' => array( + 'method' => 'POST', + 'protocol_version' => 1.1, + 'timeout' => self::HTTP_TIMEOUT, + 'max_redirects' => self::HTTP_MAX_REDIRECTS, + 'header' => implode("\r\n", $headers), + 'content' => $content + ) + )); + + $stream = @fopen(trim($url), 'r', false, $context); + $response = ''; + + if (is_resource($stream)) { + $response = stream_get_contents($stream); + } + else { + $this->container['logger']->error('HttpClient: request failed'); + } + + if (DEBUG) { + $this->container['logger']->debug('HttpClient: url='.$url); + $this->container['logger']->debug('HttpClient: payload='.$content); + $this->container['logger']->debug('HttpClient: metadata='.var_export(@stream_get_meta_data($stream), true)); + $this->container['logger']->debug('HttpClient: response='.$response); + } + + return $response; + } +} diff --git a/app/Core/Listener.php b/app/Core/Listener.php deleted file mode 100644 index 9c96cd57..00000000 --- a/app/Core/Listener.php +++ /dev/null @@ -1,21 +0,0 @@ -<?php - -namespace Core; - -/** - * Event listener interface - * - * @package core - * @author Frederic Guillot - */ -interface Listener -{ - /** - * Execute the listener - * - * @access public - * @param array $data Event data - * @return boolean - */ - public function execute(array $data); -} diff --git a/app/Core/Loader.php b/app/Core/Loader.php deleted file mode 100644 index 151081c1..00000000 --- a/app/Core/Loader.php +++ /dev/null @@ -1,62 +0,0 @@ -<?php - -namespace Core; - -/** - * Loader class - * - * @package core - * @author Frederic Guillot - */ -class Loader -{ - /** - * List of paths - * - * @access private - * @var array - */ - private $paths = array(); - - /** - * Load the missing class - * - * @access public - * @param string $class Class name with namespace - */ - public function load($class) - { - foreach ($this->paths as $path) { - - $filename = $path.DIRECTORY_SEPARATOR.str_replace('\\', DIRECTORY_SEPARATOR, $class).'.php'; - - if (file_exists($filename)) { - require $filename; - break; - } - } - } - - /** - * Register the autoloader - * - * @access public - */ - public function execute() - { - spl_autoload_register(array($this, 'load')); - } - - /** - * Register a new path - * - * @access public - * @param string $path Path - * @return Core\Loader - */ - public function setPath($path) - { - $this->paths[] = $path; - return $this; - } -} diff --git a/app/Core/Markdown.php b/app/Core/Markdown.php new file mode 100644 index 00000000..fa4e8080 --- /dev/null +++ b/app/Core/Markdown.php @@ -0,0 +1,47 @@ +<?php + +namespace Core; + +use Parsedown; +use Helper\Url; + +/** + * Specific Markdown rules for Kanboard + * + * @package core + * @author norcnorc + * @author Frederic Guillot + */ +class Markdown extends Parsedown +{ + private $link; + private $helper; + + public function __construct($link, Url $helper) + { + $this->link = $link; + $this->helper = $helper; + $this->InlineTypes['#'][] = 'TaskLink'; + $this->inlineMarkerList .= '#'; + } + + protected function inlineTaskLink($Excerpt) + { + // Replace task #123 by a link to the task + if (! empty($this->link) && preg_match('!#(\d+)!i', $Excerpt['text'], $matches)) { + + $url = $this->helper->href( + $this->link['controller'], + $this->link['action'], + $this->link['params'] + array('task_id' => $matches[1]) + ); + + return array( + 'extent' => strlen($matches[0]), + 'element' => array( + 'name' => 'a', + 'text' => $matches[0], + 'attributes' => array('href' => $url))); + } + } +} diff --git a/app/Core/MemoryCache.php b/app/Core/MemoryCache.php new file mode 100644 index 00000000..f80a66ef --- /dev/null +++ b/app/Core/MemoryCache.php @@ -0,0 +1,32 @@ +<?php + +namespace Core; + +class MemoryCache extends Cache +{ + private $storage = array(); + + public function init() + { + } + + public function set($key, $value) + { + $this->storage[$key] = $value; + } + + public function get($key) + { + return isset($this->storage[$key]) ? $this->storage[$key] : null; + } + + public function flush() + { + $this->storage = array(); + } + + public function remove($key) + { + unset($this->storage[$key]); + } +} diff --git a/app/Core/Paginator.php b/app/Core/Paginator.php new file mode 100644 index 00000000..12cc05a1 --- /dev/null +++ b/app/Core/Paginator.php @@ -0,0 +1,461 @@ +<?php + +namespace Core; + +use Pimple\Container; +use PicoDb\Table; + +/** + * Paginator helper + * + * @package core + * @author Frederic Guillot + */ +class Paginator +{ + /** + * Container instance + * + * @access private + * @var \Pimple\Container + */ + private $container; + + /** + * Total number of items + * + * @access private + * @var integer + */ + private $total = 0; + + /** + * Page number + * + * @access private + * @var integer + */ + private $page = 1; + + /** + * Offset + * + * @access private + * @var integer + */ + private $offset = 0; + + /** + * Limit + * + * @access private + * @var integer + */ + private $limit = 0; + + /** + * Sort by this column + * + * @access private + * @var string + */ + private $order = ''; + + /** + * Sorting direction + * + * @access private + * @var string + */ + private $direction = 'ASC'; + + /** + * Slice of items + * + * @access private + * @var array + */ + private $items = array(); + + /** + * PicoDb Table instance + * + * @access private + * @var \Picodb\Table + */ + private $query = null; + + /** + * Controller name + * + * @access private + * @var string + */ + private $controller = ''; + + /** + * Action name + * + * @access private + * @var string + */ + private $action = ''; + + /** + * Url params + * + * @access private + * @var array + */ + private $params = array(); + + /** + * Constructor + * + * @access public + * @param \Pimple\Container $container + */ + public function __construct(Container $container) + { + $this->container = $container; + } + + /** + * Set a PicoDb query + * + * @access public + * @param \PicoDb\Table + * @return Paginator + */ + public function setQuery(Table $query) + { + $this->query = $query; + $this->total = $this->query->count(); + return $this; + } + + /** + * Execute a PicoDb query + * + * @access public + * @return array + */ + public function executeQuery() + { + if ($this->query !== null) { + return $this->query + ->offset($this->offset) + ->limit($this->limit) + ->orderBy($this->order, $this->direction) + ->findAll(); + } + + return array(); + } + + /** + * Set url parameters + * + * @access public + * @param string $controller + * @param string $action + * @param array $params + * @return Paginator + */ + public function setUrl($controller, $action, array $params = array()) + { + $this->controller = $controller; + $this->action = $action; + $this->params = $params; + return $this; + } + + /** + * Add manually items + * + * @access public + * @param array $items + * @return Paginator + */ + public function setCollection(array $items) + { + $this->items = $items; + return $this; + } + + /** + * Return the items + * + * @access public + * @return array + */ + public function getCollection() + { + return $this->items ?: $this->executeQuery(); + } + + /** + * Set the total number of items + * + * @access public + * @param integer $total + * @return Paginator + */ + public function setTotal($total) + { + $this->total = $total; + return $this; + } + + /** + * Get the total number of items + * + * @access public + * @return integer + */ + public function getTotal() + { + return $this->total; + } + + /** + * Set the default page number + * + * @access public + * @param integer $page + * @return Paginator + */ + public function setPage($page) + { + $this->page = $page; + return $this; + } + + /** + * Set the default column order + * + * @access public + * @param string $order + * @return Paginator + */ + public function setOrder($order) + { + $this->order = $order; + return $this; + } + + /** + * Set the default sorting direction + * + * @access public + * @param string $direction + * @return Paginator + */ + public function setDirection($direction) + { + $this->direction = $direction; + return $this; + } + + /** + * Set the maximum number of items per page + * + * @access public + * @param integer $limit + * @return Paginator + */ + public function setMax($limit) + { + $this->limit = $limit; + return $this; + } + + /** + * Return true if the collection is empty + * + * @access public + * @return boolean + */ + public function isEmpty() + { + return $this->total === 0; + } + + /** + * Execute the offset calculation only if the $condition is true + * + * @access public + * @param boolean $condition + * @return Paginator + */ + public function calculateOnlyIf($condition) + { + if ($condition) { + $this->calculate(); + } + + return $this; + } + + /** + * Calculate the offset value accoring to url params and the page number + * + * @access public + * @return Paginator + */ + public function calculate() + { + $this->page = $this->container['request']->getIntegerParam('page', 1); + $this->direction = $this->container['request']->getStringParam('direction', $this->direction); + $this->order = $this->container['request']->getStringParam('order', $this->order); + + if ($this->page < 1) { + $this->page = 1; + } + + $this->offset = ($this->page - 1) * $this->limit; + + return $this; + } + + /** + * Get url params for link generation + * + * @access public + * @param integer $page + * @param string $order + * @param string $direction + * @return string + */ + public function getUrlParams($page, $order, $direction) + { + $params = array( + 'page' => $page, + 'order' => $order, + 'direction' => $direction, + ); + + return array_merge($this->params, $params); + } + + /** + * Generate the previous link + * + * @access public + * @return string + */ + public function generatePreviousLink() + { + $html = '<span class="pagination-previous">'; + + if ($this->offset > 0) { + $html .= $this->container['helper']->url->link( + '← '.t('Previous'), + $this->controller, + $this->action, + $this->getUrlParams($this->page - 1, $this->order, $this->direction) + ); + } + else { + $html .= '← '.t('Previous'); + } + + $html .= '</span>'; + + return $html; + } + + /** + * Generate the next link + * + * @access public + * @return string + */ + public function generateNextLink() + { + $html = '<span class="pagination-next">'; + + if (($this->total - $this->offset) > $this->limit) { + $html .= $this->container['helper']->url->link( + t('Next').' →', + $this->controller, + $this->action, + $this->getUrlParams($this->page + 1, $this->order, $this->direction) + ); + } + else { + $html .= t('Next').' →'; + } + + $html .= '</span>'; + + return $html; + } + + /** + * Return true if there is no pagination to show + * + * @access public + * @return boolean + */ + public function hasNothingtoShow() + { + return $this->offset === 0 && ($this->total - $this->offset) <= $this->limit; + } + + /** + * Generation pagination links + * + * @access public + * @return string + */ + public function toHtml() + { + $html = ''; + + if (! $this->hasNothingtoShow()) { + $html .= '<div class="pagination">'; + $html .= $this->generatePreviousLink(); + $html .= $this->generateNextLink(); + $html .= '</div>'; + } + + return $html; + } + + /** + * Magic method to output pagination links + * + * @access public + * @return string + */ + public function __toString() + { + return $this->toHtml(); + } + + /** + * Column sorting + * + * @param string $label Column title + * @param string $column SQL column name + * @return string + */ + public function order($label, $column) + { + $prefix = ''; + $direction = 'ASC'; + + if ($this->order === $column) { + $prefix = $this->direction === 'DESC' ? '▼ ' : '▲ '; + $direction = $this->direction === 'DESC' ? 'ASC' : 'DESC'; + } + + return $prefix.$this->container['helper']->url->link( + $label, + $this->controller, + $this->action, + $this->getUrlParams($this->page, $column, $direction) + ); + } +} diff --git a/app/Core/Registry.php b/app/Core/Registry.php deleted file mode 100644 index d8b9063e..00000000 --- a/app/Core/Registry.php +++ /dev/null @@ -1,83 +0,0 @@ -<?php - -namespace Core; - -use RuntimeException; - -/** - * The registry class is a dependency injection container - * - * @property mixed db - * @property mixed event - * @package core - * @author Frederic Guillot - */ -class Registry -{ - /** - * Contains all dependencies - * - * @access private - * @var array - */ - private $container = array(); - - /** - * Contains all instances - * - * @access private - * @var array - */ - private $instances = array(); - - /** - * Set a dependency - * - * @access public - * @param string $name Unique identifier for the service/parameter - * @param mixed $value The value of the parameter or a closure to define an object - */ - public function __set($name, $value) - { - $this->container[$name] = $value; - } - - /** - * Get a dependency - * - * @access public - * @param string $name Unique identifier for the service/parameter - * @return mixed The value of the parameter or an object - * @throws RuntimeException If the identifier is not found - */ - public function __get($name) - { - if (isset($this->container[$name])) { - - if (is_callable($this->container[$name])) { - return $this->container[$name](); - } - else { - return $this->container[$name]; - } - } - - throw new \RuntimeException('Identifier not found in the registry: '.$name); - } - - /** - * Return a shared instance of a dependency - * - * @access public - * @param string $name Unique identifier for the service/parameter - * @return mixed Same object instance of the dependency - */ - public function shared($name) - { - if (! isset($this->instances[$name])) { - $this->instances[$name] = $this->$name; - } - - return $this->instances[$name]; - } -} diff --git a/app/Core/Request.php b/app/Core/Request.php index a4c426f0..c7ca3184 100644 --- a/app/Core/Request.php +++ b/app/Core/Request.php @@ -76,6 +76,17 @@ class Request } /** + * Get the Json request body + * + * @access public + * @return array + */ + public function getJson() + { + return json_decode($this->getBody(), true); + } + + /** * Get the content of an uploaded file * * @access public @@ -114,6 +125,20 @@ class Request } /** + * Check if the page is requested through HTTPS + * + * Note: IIS return the value 'off' and other web servers an empty value when it's not HTTPS + * + * @static + * @access public + * @return boolean + */ + public static function isHTTPS() + { + return isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== '' && $_SERVER['HTTPS'] !== 'off'; + } + + /** * Return a HTTP header value * * @access public diff --git a/app/Core/Response.php b/app/Core/Response.php index 347cdde7..d42a8f1e 100644 --- a/app/Core/Response.php +++ b/app/Core/Response.php @@ -168,6 +168,23 @@ class Response } /** + * Send a css response + * + * @access public + * @param string $data Raw data + * @param integer $status_code HTTP status code + */ + public function css($data, $status_code = 200) + { + $this->status($status_code); + + header('Content-Type: text/css; charset=utf-8'); + echo $data; + + exit; + } + + /** * Send a binary response * * @access public @@ -195,24 +212,7 @@ class Response $policies['default-src'] = "'self'"; $values = ''; - foreach ($policies as $policy => $hosts) { - - if (is_array($hosts)) { - - $acl = ''; - - foreach ($hosts as &$host) { - - if ($host === '*' || $host === 'self' || strpos($host, 'http') === 0) { - $acl .= $host.' '; - } - } - } - else { - - $acl = $hosts; - } - + foreach ($policies as $policy => $acl) { $values .= $policy.' '.trim($acl).'; '; } @@ -246,7 +246,7 @@ class Response */ public function hsts() { - if (Tool::isHTTPS()) { + if (Request::isHTTPS()) { header('Strict-Transport-Security: max-age=31536000'); } } diff --git a/app/Core/Router.php b/app/Core/Router.php index c9af6e2c..36c11a0a 100644 --- a/app/Core/Router.php +++ b/app/Core/Router.php @@ -2,6 +2,8 @@ namespace Core; +use Pimple\Container; + /** * Router class * @@ -27,24 +29,24 @@ class Router private $action = ''; /** - * Registry instance + * Container instance * * @access private - * @var \Core\Registry + * @var \Pimple\Container */ - private $registry; + private $container; /** * Constructor * * @access public - * @param Registry $registry Registry instance - * @param string $controller Controller name - * @param string $action Action name + * @param \Pimple\Container $container Container instance + * @param string $controller Controller name + * @param string $action Action name */ - public function __construct(Registry $registry, $controller = '', $action = '') + public function __construct(Container $container, $controller = '', $action = '') { - $this->registry = $registry; + $this->container = $container; $this->controller = empty($_GET['controller']) ? $controller : $_GET['controller']; $this->action = empty($_GET['action']) ? $action : $_GET['action']; } @@ -81,11 +83,7 @@ class Router return false; } - $instance = new $class($this->registry); - $instance->request = new Request; - $instance->response = new Response; - $instance->session = new Session; - $instance->template = new Template; + $instance = new $class($this->container); $instance->beforeAction($this->controller, $this->action); $instance->$method(); diff --git a/app/Core/Session.php b/app/Core/Session.php index 6028f0b9..c35014cd 100644 --- a/app/Core/Session.php +++ b/app/Core/Session.php @@ -2,13 +2,15 @@ namespace Core; +use ArrayAccess; + /** * Session class * * @package core * @author Frederic Guillot */ -class Session +class Session implements ArrayAccess { /** * Sesion lifetime @@ -36,32 +38,32 @@ class Session * * @access public * @param string $base_path Cookie path - * @param string $save_path Custom session save path */ - public function open($base_path = '/', $save_path = '') + public function open($base_path = '/') { - if ($save_path !== '') { - session_save_path($save_path); - } - + $base_path = str_replace('\\', '/', $base_path); + // HttpOnly and secure flags for session cookie session_set_cookie_params( self::SESSION_LIFETIME, $base_path ?: '/', null, - Tool::isHTTPS(), + Request::isHTTPS(), true ); // Avoid session id in the URL ini_set('session.use_only_cookies', '1'); + // Enable strict mode + ini_set('session.use_strict_mode', '1'); + // Ensure session ID integrity ini_set('session.entropy_file', '/dev/urandom'); ini_set('session.entropy_length', '32'); ini_set('session.hash_bits_per_character', 6); - // If session was autostarted with session.auto_start = 1 in php.ini destroy it, otherwise we cannot login + // If the session was autostarted with session.auto_start = 1 in php.ini destroy it if (isset($_SESSION)) { session_destroy(); } @@ -90,19 +92,17 @@ class Session $_SESSION = array(); // Destroy the session cookie - if (ini_get('session.use_cookies')) { - $params = session_get_cookie_params(); - - setcookie( - session_name(), - '', - time() - 42000, - $params['path'], - $params['domain'], - $params['secure'], - $params['httponly'] - ); - } + $params = session_get_cookie_params(); + + setcookie( + session_name(), + '', + time() - 42000, + $params['path'], + $params['domain'], + $params['secure'], + $params['httponly'] + ); // Destroy session data session_destroy(); @@ -129,4 +129,24 @@ class Session { $_SESSION['flash_error_message'] = $message; } + + public function offsetSet($offset, $value) + { + $_SESSION[$offset] = $value; + } + + public function offsetExists($offset) + { + return isset($_SESSION[$offset]); + } + + public function offsetUnset($offset) + { + unset($_SESSION[$offset]); + } + + public function offsetGet($offset) + { + return isset($_SESSION[$offset]) ? $_SESSION[$offset] : null; + } } diff --git a/app/Core/Template.php b/app/Core/Template.php index f21e8a6d..9688c2a5 100644 --- a/app/Core/Template.php +++ b/app/Core/Template.php @@ -10,28 +10,28 @@ use LogicException; * @package core * @author Frederic Guillot */ -class Template +class Template extends Helper { /** * Template path * * @var string */ - const PATH = 'app/Templates/'; + const PATH = 'app/Template/'; /** - * Load a template + * Render a template * * Example: * - * $template->load('template_name', ['bla' => 'value']); + * $template->render('template_name', ['bla' => 'value']); * * @access public * @params string $__template_name Template name * @params array $__template_args Key/Value map of template variables * @return string */ - public function load($__template_name, array $__template_args = array()) + public function render($__template_name, array $__template_args = array()) { $__template_file = self::PATH.$__template_name.'.php'; @@ -57,9 +57,9 @@ class Template */ public function layout($template_name, array $template_args = array(), $layout_name = 'layout') { - return $this->load( + return $this->render( $layout_name, - $template_args + array('content_for_layout' => $this->load($template_name, $template_args)) + $template_args + array('content_for_layout' => $this->render($template_name, $template_args)) ); } } diff --git a/app/Core/Tool.php b/app/Core/Tool.php index e54a0d3b..84e42ba8 100644 --- a/app/Core/Tool.php +++ b/app/Core/Tool.php @@ -33,35 +33,22 @@ class Tool } /** - * Load and register a model + * Get the mailbox hash from an email address * * @static * @access public - * @param Core\Registry $registry DPI container - * @param string $name Model name - * @return mixed + * @param string $email + * @return string */ - public static function loadModel(Registry $registry, $name) + public static function getMailboxHash($email) { - if (! isset($registry->$name)) { - $class = '\Model\\'.ucfirst($name); - $registry->$name = new $class($registry); + if (! strpos($email, '@') || ! strpos($email, '+')) { + return ''; } - return $registry->shared($name); - } + list($local_part,) = explode('@', $email); + list(,$identifier) = explode('+', $local_part); - /** - * Check if the page is requested through HTTPS - * - * Note: IIS return the value 'off' and other web servers an empty value when it's not HTTPS - * - * @static - * @access public - * @return boolean - */ - public static function isHTTPS() - { - return isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== '' && $_SERVER['HTTPS'] !== 'off'; + return $identifier; } } diff --git a/app/Core/Translator.php b/app/Core/Translator.php index 43e934a9..e3d19692 100644 --- a/app/Core/Translator.php +++ b/app/Core/Translator.php @@ -11,14 +11,14 @@ namespace Core; class Translator { /** - * Locales path + * Locale path * * @var string */ - const PATH = 'app/Locales/'; + const PATH = 'app/Locale/'; /** - * Locales + * Locale * * @static * @access private @@ -27,6 +27,31 @@ class Translator private static $locales = array(); /** + * Instance + * + * @static + * @access private + * @var Translator + */ + private static $instance = null; + + /** + * Get instance + * + * @static + * @access public + * @return Translator + */ + public static function getInstance() + { + if (self::$instance === null) { + self::$instance = new self; + } + + return self::$instance; + } + + /** * Get a translation * * $translator->translate('I have %d kids', 5); @@ -181,5 +206,8 @@ class Translator if (file_exists($filename)) { self::$locales = require $filename; } + else { + self::$locales = array(); + } } } |