Merge branch 'master' into feature/skip-content-7933

This commit is contained in:
Marius Hein 2015-02-12 13:14:56 +01:00
commit 09349288e1
66 changed files with 7223 additions and 1090 deletions

View File

@ -10,29 +10,22 @@
#
# icinga2::feature { 'example-feature'; }
#
define icinga2::feature ($source = undef) {
define icinga2::feature ($ensure = 'present') {
include icinga2
$target = "features-available/${name}"
$cfgpath = '/etc/icinga2'
$path = "${cfgpath}/features-enabled/${name}.conf"
if $source != undef {
icinga2::config { $target:
source => $source,
}
$action = $ensure ? {
/(present)/ => 'enable',
/(absent)/ => 'disable',
}
$test = $ensure ? {
/(present)/ => '-e',
/(absent)/ => '! -e',
}
parent_dirs { $path:
user => 'icinga',
require => [
User['icinga'],
File['icinga2cfgDir']
],
}
-> file { $path:
ensure => link,
target => "${cfgpath}/${target}.conf",
exec { "icinga2-feature-${action}-${name}":
unless => "/usr/bin/test ${test} /etc/icinga2/features-enabled/${name}.conf",
command => "/usr/sbin/icinga2 feature ${action} ${name}",
require => Package['icinga2'],
notify => Service['icinga2'],
}
}

View File

@ -28,7 +28,8 @@ class icinga2_mysql {
privileges => 'SELECT,INSERT,UPDATE,DELETE',
schemafile => '/usr/share/icinga2-ido-mysql/schema/mysql.sql',
}
-> icinga2::feature { 'ido-mysql':
-> icinga2::config { 'features-available/ido-mysql':
source => 'puppet:///modules/icinga2_mysql',
}
-> icinga2::feature { 'ido-mysql': }
}

View File

@ -6,6 +6,7 @@
#
# icinga2_mysql
# icinga2::config
# icinga2::feature
#
# Sample Usage:
#
@ -21,4 +22,9 @@ class icinga2_dev {
'conf.d/test-config', 'conf.d/commands', 'constants' ]:
source => 'puppet:///modules/icinga2_dev',
}
icinga2::feature { 'ido-pgsql':
ensure => absent,
require => Class['icinga2_pgsql'],
}
}

View File

@ -1,9 +1,11 @@
Alexander Fuhr <alexander.fuhr@netways.de>
Alexander Klimov <alexander.klimov@netways.de>
baufrecht <baufrecht@users.noreply.github.com>
Bernd Erk <bernd.erk@icinga.org>
Boden Garman <boden.garman@spintel.net.au>
Carlos Cesario <carloscesario@gmail.com>
Chris Rüll <christopher.ruell@netways.de>
Davide Demuru <davide.demuru@buongiorno.com>
Eric Lippmann <eric.lippmann@netways.de>
Goran Rakic <grakic@devbase.net>
Gunnar Beutner <gunnar.beutner@netways.de>

View File

@ -141,7 +141,7 @@ class AuthenticationController extends ActionController
}
}
} catch (Exception $e) {
$this->view->errorInfo = $e->getMessage();
$this->view->form->addError($e->getMessage());
}
$this->view->requiresExternalAuth = $triedOnlyExternalAuth && !$auth->isAuthenticated();

View File

@ -40,13 +40,22 @@ class ExternalBackendForm extends Form
array(
'pattern' => '/^[^\\[\\]:]+$/',
'messages' => array(
'regexNotMatch' => 'The backend name cannot contain \'[\', \']\' or \':\'.'
'regexNotMatch' => $this->translate(
'The backend name cannot contain \'[\', \']\' or \':\'.'
)
)
)
)
)
)
);
$callbackValidator = new Zend_Validate_Callback(function ($value) {
return @preg_match($value, '') !== false;
});
$callbackValidator->setMessage(
$this->translate('"%value%" is not a valid regular expression'),
Zend_Validate_Callback::INVALID_VALUE
);
$this->addElement(
'text',
'strip_username_regexp',
@ -56,11 +65,7 @@ class ExternalBackendForm extends Form
'The regular expression to use to strip specific parts off from usernames.'
. ' Leave empty if you do not want to strip off anything'
),
'validators' => array(
new Zend_Validate_Callback(function ($value) {
return @preg_match($value, '') !== false;
})
)
'validators' => array($callbackValidator)
)
);
$this->addElement(

View File

@ -5,7 +5,6 @@ namespace Icinga\Forms\Config\General;
use Icinga\Application\Logger;
use Icinga\Web\Form;
use Icinga\Web\Form\Validator\WritablePathValidator;
class LoggingConfigForm extends Form
{
@ -75,7 +74,9 @@ class LoggingConfigForm extends Form
array(
'pattern' => '/^[^\W]+$/',
'messages' => array(
'regexNotMatch' => 'The application prefix cannot contain any whitespaces.'
'regexNotMatch' => $this->translate(
'The application prefix must not contain whitespace.'
)
)
)
)
@ -107,7 +108,7 @@ class LoggingConfigForm extends Form
'label' => $this->translate('File path'),
'description' => $this->translate('The full path to the log file to write messages to.'),
'value' => '/var/log/icingaweb2/icingaweb2.log',
'validators' => array(new WritablePathValidator())
'validators' => array('WritablePathValidator')
)
);
}

View File

@ -4,7 +4,6 @@
namespace Icinga\Forms\Config\Resource;
use Icinga\Web\Form;
use Icinga\Web\Form\Validator\ReadablePathValidator;
/**
* Form class for adding/modifying file resources
@ -40,7 +39,7 @@ class FileResourceForm extends Form
'required' => true,
'label' => $this->translate('Filepath'),
'description' => $this->translate('The filename to fetch information from'),
'validators' => array(new ReadablePathValidator())
'validators' => array('ReadablePathValidator')
)
);
$this->addElement(

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -28,8 +28,8 @@
Enter some text
<span class="required-indicator" aria-hidden="true"></span>
<span class="sr-only"> (required)</span>
<input type="text" name="some_text" value="" aria-required="true" required>
</label>
<input type="text" name="some_text" value="" aria-required="true" required>
<input type="submit" name="btn_submit" value="Submit">
</form>
</body>

View File

@ -40,7 +40,7 @@ or LDAP configuration method.
Directive | Description
------------------------|------------
**backend** | `ldap`
**resource** | The name of the LDAP resource defined in [resources.ini](resources).
**resource** | The name of the LDAP resource defined in [resources.ini](#resources).
**user_class** | LDAP user class.
**user_name_attribute** | LDAP attribute which contains the username.
@ -63,7 +63,7 @@ with Icinga Web 2 (e.g. an alias) no matter what the primary user id might actua
Directive | Description
------------------------|------------
**backend** | `ad`
**resource** | The name of the LDAP resource defined in [resources.ini](resources).
**resource** | The name of the LDAP resource defined in [resources.ini](#resources).
**Example:**
@ -82,7 +82,7 @@ authentication method.
Directive | Description
------------------------|------------
**backend** | `db`
**resource** | The name of the database resource defined in [resources.ini](resources).
**resource** | The name of the database resource defined in [resources.ini](#resources).
**Example:**

View File

@ -94,7 +94,7 @@ usermod -A icingaweb2 wwwrun
Debian and Ubuntu:
````
usermod -a -G icingaweb2 wwwrun
usermod -a -G icingaweb2 www-data
````
Use `icingacli` to create the configuration directory which defaults to **/etc/icingaweb2**:

View File

@ -1,78 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Data\Tree;
use SplDoublyLinkedList;
class Node extends SplDoublyLinkedList implements NodeInterface
{
/**
* The node's value
*
* @var mixed
*/
protected $value;
/**
* Create a new node
*
* @param mixed $value The node's value
*/
public function __construct($value = null)
{
$this->value = $value;
}
/**
* Get the node's value
*
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* Create a new node from the given value and insert the node as the last child of this node
*
* @param mixed $value The node's value
*
* @return NodeInterface The appended node
*/
public function appendChild($value)
{
$child = new static($value);
$this->push($child);
return $child;
}
/**
* Whether this node has child nodes
*
* @return bool
*/
public function hasChildren()
{
$current = $this->current();
if ($current === null) {
$current = $this;
}
return ! $current->isEmpty();
}
/**
* Get the node's child nodes
*
* @return NodeInterface
*/
public function getChildren()
{
$current = $this->current();
if ($current === null) {
$current = $this;
}
return $current;
}
}

View File

@ -1,25 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Data\Tree;
use RecursiveIterator;
interface NodeInterface extends RecursiveIterator
{
/**
* Create a new node from the given value and insert the node as the last child of this node
*
* @param mixed $value The node's value
*
* @return NodeInterface The appended node
*/
public function appendChild($value);
/**
* Get the node's value
*
* @return mixed
*/
public function getValue();
}

View File

@ -0,0 +1,90 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Data\Tree;
use IteratorAggregate;
use LogicException;
/**
* A simple tree
*/
class SimpleTree implements IteratorAggregate
{
/**
* Root node
*
* @type TreeNode
*/
protected $sentinel;
/**
* Nodes
*
* @type array
*/
protected $nodes = array();
/**
* Create a new simple tree
*/
public function __construct()
{
$this->sentinel = new TreeNode();
}
/**
* Add a child node
*
* @param TreeNode $child
* @param TreeNode $parent
*
* @return $this
*/
public function addChild(TreeNode $child, TreeNode $parent = null)
{
if ($parent === null) {
$parent = $this->sentinel;
} elseif (! isset($this->nodes[$parent->getId()])) {
throw new LogicException(sprintf(
'Can\'t append child node %s to parent node %s: Parent node does not exist',
$child->getId(),
$parent->getId()
));
}
if (isset($this->nodes[$child->getId()])) {
throw new LogicException(sprintf(
'Can\'t append child node %s to parent node %s: Child node does already exist',
$child->getId(),
$parent->getId()
));
}
$this->nodes[$child->getId()] = $child;
$parent->appendChild($child);
return $this;
}
/**
* Get a node by its ID
*
* @param mixed $id
*
* @return TreeNode|null
*/
public function getNode($id)
{
if (! isset($this->nodes[$id])) {
return null;
}
return $this->nodes[$id];
}
/**
* {@inheritdoc}
* @return TreeNodeIterator
*/
public function getIterator()
{
return new TreeNodeIterator($this->sentinel);
}
}

View File

@ -0,0 +1,109 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Data\Tree;
use Icinga\Data\Identifiable;
class TreeNode implements Identifiable
{
/**
* The node's ID
*
* @type mixed
*/
protected $id;
/**
* The node's value
*
* @var mixed
*/
protected $value;
/**
* The node's children
*
* @type array
*/
protected $children = array();
/**
* Set the node's ID
*
* @param mixed $id ID of the node
*
* @return $this
*/
public function setId($id)
{
$this->id = $id;
return $this;
}
/**
* (non-PHPDoc)
* @see Identifiable::getId() For the method documentation.
*/
public function getId()
{
return $this->id;
}
/**
* Set the node's value
*
* @param mixed $value
*
* @return $this
*/
public function setValue($value)
{
$this->value = $value;
return $this;
}
/**
* Get the node's value
*
* @return mixed
*/
public function getValue()
{
return $this->value;
}
/**
* Append a child node as the last child of this node
*
* @param TreeNode $child The child to append
*
* @return $this
*/
public function appendChild(TreeNode $child)
{
$this->children[] = $child;
return $this;
}
/**
* Get whether the node has children
*
* @return bool
*/
public function hasChildren()
{
return ! empty($this->children);
}
/**
* Get the node's children
*
* @return array
*/
public function getChildren()
{
return $this->children;
}
}

View File

@ -0,0 +1,87 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Data\Tree;
use ArrayIterator;
use RecursiveIterator;
/**
* Iterator over a tree node's children
*/
class TreeNodeIterator implements RecursiveIterator
{
/**
* The node's children
*
* @type array
*/
protected $children;
/**
* Create a new iterator over a tree node's children
*
* @param TreeNode $node
*/
public function __construct(TreeNode $node)
{
$this->children = new ArrayIterator($node->getChildren());
}
/**
* {@inheritdoc}
*/
public function current()
{
return $this->children->current();
}
/**
* {@inheritdoc}
*/
public function key()
{
return $this->children->key();
}
/**
* {@inheritdoc}
*/
public function next()
{
$this->children->next();
}
/**
* {@inheritdoc}
*/
public function rewind()
{
$this->children->rewind();
}
/**
* {@inheritdoc}
*/
public function valid()
{
return $this->children->valid();
}
/**
* {@inheritdoc}
*/
public function hasChildren()
{
return $this->current()->hasChildren();
}
/**
* {@inheritdoc}
* @return TreeNodeIterator
*/
public function getChildren()
{
return new static($this->current());
}
}

View File

@ -58,7 +58,7 @@ class FileExtensionFilterIterator extends FilterIterator
public function accept()
{
$current = $this->current();
/* @var $current \SplFileInfo */
/** @type $current \SplFileInfo */
if (! $current->isFile()) {
return false;
}

View File

@ -37,19 +37,22 @@ class Pdf extends DOMPDF
public function renderControllerAction($controller)
{
$this->assertNoHeadersSent();
ini_set('memory_limit', '384M');
ini_set('max_execution_time', 300);
$request = $controller->getRequest();
$viewRenderer = $controller->getHelper('viewRenderer');
$controller->render(
$viewRenderer->getScriptAction(),
$viewRenderer->getResponseSegment(),
$viewRenderer->getNoController()
);
$layout = $controller->getHelper('layout')->setLayout('pdf');
$controller->render();
$layout->content = $controller->getResponse();
$html = $layout->render();
$imgDir = Url::fromPath('img');
$html = preg_replace('~src="' . $imgDir . '/~', 'src="' . Icinga::app()->getBootstrapDirectory() . '/img/', $html);
$this->load_html($html);
$this->render();
$request = $controller->getRequest();
$this->stream(
sprintf(
'%s-%s-%d.pdf',

View File

@ -53,4 +53,30 @@ class String
return $string;
}
/**
* Find and return all similar strings in $possibilites matching $string with the given minimum $similarity
*
* @param string $string
* @param array $possibilities
* @param float $similarity
*
* @return array
*/
public static function findSimilar($string, array $possibilities, $similarity = 0.33)
{
if (empty($string)) {
return array();
}
$matches = array();
foreach ($possibilities as $possibility) {
$distance = levenshtein($string, $possibility);
if ($distance / strlen($string) <= $similarity) {
$matches[] = $possibility;
}
}
return $matches;
}
}

View File

@ -402,7 +402,6 @@ class ActionController extends Zend_Controller_Action
}
if ($req->getParam('format') === 'pdf') {
$layout->setLayout('pdf');
$this->shutdownSession();
$this->sendAsPdf();
exit;

View File

@ -0,0 +1,106 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Dom;
use DOMNode;
use IteratorIterator;
use RecursiveIterator;
/**
* Recursive iterator over a DOMNode
*
* Usage example:
* <code>
* <?php
*
* namespace Icinga\Example;
*
* use DOMDocument;
* use RecursiveIteratorIterator;
* use Icinga\Web\Dom\DomIterator;
*
* $doc = new DOMDocument();
* $doc->loadHTML(...);
* $dom = new RecursiveIteratorIterator(new DomNodeIterator($doc), RecursiveIteratorIterator::SELF_FIRST);
* foreach ($dom as $node) {
* ....
* }
* </code>
*/
class DomNodeIterator implements RecursiveIterator
{
/**
* The node's children
*
* @type IteratorIterator
*/
protected $children;
/**
* Create a new iterator over a DOMNode's children
*
* @param DOMNode $node
*/
public function __construct(DOMNode $node)
{
$this->children = new IteratorIterator($node->childNodes);
}
/**
* {@inheritdoc}
*/
public function current()
{
return $this->children->current();
}
/**
* {@inheritdoc}
*/
public function key()
{
return $this->children->key();
}
/**
* {@inheritdoc}
*/
public function next()
{
$this->children->next();
}
/**
* {@inheritdoc}
*/
public function rewind()
{
$this->children->rewind();
}
/**
* {@inheritdoc}
*/
public function valid()
{
return $this->children->valid();
}
/**
* {@inheritdoc}
*/
public function hasChildren()
{
return $this->current()->hasChildNodes();
}
/**
* {@inheritdoc}
* @return DomNodeIterator
*/
public function getChildren()
{
return new static($this->current());
}
}

View File

@ -12,6 +12,7 @@ use Icinga\Application\Icinga;
use Icinga\Authentication\Manager;
use Icinga\Security\SecurityException;
use Icinga\Util\Translator;
use Icinga\Web\Form\ErrorLabeller;
use Icinga\Web\Form\Decorator\NoScriptApply;
use Icinga\Web\Form\Element\CsrfCounterMeasure;
@ -520,6 +521,15 @@ class Form extends Zend_Form
}
$el = parent::createElement($type, $name, $options);
$el->setTranslator(new ErrorLabeller(array('element' => $el)));
$el->addPrefixPaths(array(
array(
'prefix' => 'Icinga\\Web\\Form\\Validator\\',
'path' => Icinga::app()->getLibraryDir('Icinga/Web/Form/Validator'),
'type' => $el::VALIDATE
)
));
if (($description = $el->getDescription()) !== null && ($label = $el->getDecorator('label')) !== false) {
$label->setOptions(array(

View File

@ -0,0 +1,65 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Form;
use BadMethodCallException;
use Zend_Translate_Adapter;
use Zend_Validate_NotEmpty;
use Icinga\Web\Form\Validator\DateTimeValidator;
use Icinga\Web\Form\Validator\ReadablePathValidator;
use Icinga\Web\Form\Validator\WritablePathValidator;
class ErrorLabeller extends Zend_Translate_Adapter
{
protected $messages;
public function __construct($options = array())
{
if (! isset($options['element'])) {
throw new BadMethodCallException('Option "element" is missing');
}
$this->messages = $this->createMessages($options['element']);
}
public function isTranslated($messageId, $original = false, $locale = null)
{
return array_key_exists($messageId, $this->messages);
}
public function translate($messageId, $locale = null)
{
if (array_key_exists($messageId, $this->messages)) {
return $this->messages[$messageId];
}
return $messageId;
}
protected function createMessages($element)
{
$label = $element->getLabel();
return array(
Zend_Validate_NotEmpty::IS_EMPTY => sprintf(t('%s is required and must not be empty'), $label),
WritablePathValidator::NOT_WRITABLE => sprintf(t('%s is not writable', 'config.path'), $label),
WritablePathValidator::DOES_NOT_EXIST => sprintf(t('%s does not exist', 'config.path'), $label),
ReadablePathValidator::NOT_READABLE => sprintf(t('%s is not readable', 'config.path'), $label),
DateTimeValidator::INVALID_DATETIME_FORMAT => sprintf(
t('%s not in the expected format: %%value%%'),
$label
)
);
}
protected function _loadTranslationData($data, $locale, array $options = array())
{
// nonsense, required as being abstract otherwise...
}
public function toString()
{
return 'ErrorLabeller'; // nonsense, required as being abstract otherwise...
}
}

View File

@ -5,6 +5,7 @@ namespace Icinga\Web\Form\Validator;
use DateTime;
use Zend_Validate_Abstract;
use Icinga\Util\DateTimeFactory;
/**
* Validator for date-and-time input controls
@ -13,6 +14,21 @@ use Zend_Validate_Abstract;
*/
class DateTimeValidator extends Zend_Validate_Abstract
{
const INVALID_DATETIME_TYPE = 'invalidDateTimeType';
const INVALID_DATETIME_FORMAT = 'invalidDateTimeFormat';
/**
* The messages to write on differen error states
*
* @var array
*
* @see Zend_Validate_Abstract::$_messageTemplates
*/
protected $_messageTemplates = array(
self::INVALID_DATETIME_TYPE => 'Invalid type given. Instance of DateTime or date/time string expected',
self::INVALID_DATETIME_FORMAT => 'Date/time string not in the expected format: %value%'
);
protected $local;
/**
@ -38,14 +54,14 @@ class DateTimeValidator extends Zend_Validate_Abstract
public function isValid($value, $context = null)
{
if (! $value instanceof DateTime && ! is_string($value)) {
$this->_error(t('Invalid type given. Instance of DateTime or date/time string expected'));
$this->_error(self::INVALID_DATETIME_TYPE);
return false;
}
if (is_string($value)) {
$format = $this->local === true ? 'Y-m-d\TH:i:s' : DateTime::RFC3339;
$dateTime = DateTime::createFromFormat($format, $value);
if ($dateTime === false || $dateTime->format($format) !== $value) {
$this->_error(sprintf(t('Date/time string not in the expected format %s'), $format));
$this->_error(self::INVALID_DATETIME_FORMAT, DateTimeFactory::create()->format($format));
return false;
}
}

View File

@ -0,0 +1,28 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Web\Form\Validator;
use Zend_Validate_InArray;
use Icinga\Util\String;
class InArray extends Zend_Validate_InArray
{
protected function _error($messageKey, $value = null)
{
if ($messageKey === static::NOT_IN_ARRAY) {
$matches = String::findSimilar($this->_value, $this->_haystack);
if (empty($matches)) {
$this->_messages[$messageKey] = sprintf(t('"%s" is not in the list of allowed values.'), $this->_value);
} else {
$this->_messages[$messageKey] = sprintf(
t('"%s" is not in the list of allowed values. Did you mean one of the following?: %s'),
$this->_value,
implode(', ', $matches)
);
}
} else {
parent::_error($messageKey, $value);
}
}
}

View File

@ -13,6 +13,9 @@ use Zend_Validate_Abstract;
*/
class ReadablePathValidator extends Zend_Validate_Abstract
{
const NOT_READABLE = 'notReadable';
const DOES_NOT_EXIST = 'doesNotExist';
/**
* The messages to write on different error states
*
@ -20,18 +23,10 @@ class ReadablePathValidator extends Zend_Validate_Abstract
*
* @see Zend_Validate_Abstract::$_messageTemplates
*/
protected $_messageTemplates;
/**
* Initialize this validator
*/
public function __construct()
{
$this->_messageTemplates = array(
'NOT_READABLE' => t('Path is not readable'),
'DOES_NOT_EXIST' => t('Path does not exist')
);
}
protected $_messageTemplates = array(
self::NOT_READABLE => 'Path is not readable',
self::DOES_NOT_EXIST => 'Path does not exist'
);
/**
* Check whether the given value is a readable filepath
@ -44,12 +39,13 @@ class ReadablePathValidator extends Zend_Validate_Abstract
public function isValid($value, $context = null)
{
if (false === file_exists($value)) {
$this->_error('DOES_NOT_EXIST');
$this->_error(self::DOES_NOT_EXIST);
return false;
}
if (false === is_readable($value)) {
$this->_error('NOT_READABLE');
$this->_error(self::NOT_READABLE);
return false;
}
return true;

View File

@ -10,6 +10,9 @@ use Zend_Validate_Abstract;
*/
class WritablePathValidator extends Zend_Validate_Abstract
{
const NOT_WRITABLE = 'notWritable';
const DOES_NOT_EXIST = 'doesNotExist';
/**
* The messages to write on differen error states
*
@ -18,8 +21,8 @@ class WritablePathValidator extends Zend_Validate_Abstract
* @see Zend_Validate_Abstract::$_messageTemplates
*/
protected $_messageTemplates = array(
'NOT_WRITABLE' => 'Path is not writable',
'DOES_NOT_EXIST' => 'Path does not exist'
self::NOT_WRITABLE => 'Path is not writable',
self::DOES_NOT_EXIST => 'Path does not exist'
);
/**
@ -53,7 +56,7 @@ class WritablePathValidator extends Zend_Validate_Abstract
$this->_setValue($value);
if ($this->requireExistence && !file_exists($value)) {
$this->_error('DOES_NOT_EXIST');
$this->_error(self::DOES_NOT_EXIST);
return false;
}
@ -62,7 +65,8 @@ class WritablePathValidator extends Zend_Validate_Abstract
) {
return true;
}
$this->_error('NOT_WRITABLE');
$this->_error(self::NOT_WRITABLE);
return false;
}
}

View File

@ -46,17 +46,16 @@ class Doc_IcingawebController extends DocController
*/
public function chapterAction()
{
$chapterId = $this->getParam('chapterId');
if ($chapterId === null) {
$chapter = $this->getParam('chapter');
if ($chapter === null) {
throw new Zend_Controller_Action_Exception(
sprintf($this->translate('Missing parameter \'%s\''), 'chapterId'),
sprintf($this->translate('Missing parameter %s'), 'chapter'),
404
);
}
$this->renderChapter(
$this->getPath(),
$chapterId,
'doc/icingaweb/toc',
$chapter,
'doc/icingaweb/chapter'
);
}

View File

@ -122,10 +122,10 @@ class Doc_ModuleController extends DocController
{
$module = $this->getParam('moduleName');
$this->assertModuleEnabled($module);
$chapterId = $this->getParam('chapterId');
if ($chapterId === null) {
$chapter = $this->getParam('chapter');
if ($chapter === null) {
throw new Zend_Controller_Action_Exception(
sprintf($this->translate('Missing parameter \'%s\''), 'chapterId'),
sprintf($this->translate('Missing parameter %s'), 'chapter'),
404
);
}
@ -133,8 +133,7 @@ class Doc_ModuleController extends DocController
try {
$this->renderChapter(
$this->getPath($module, Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')),
$chapterId,
$this->_helper->url->url(array('moduleName' => $module), 'doc/module/toc'),
$chapter,
'doc/module/chapter',
array('moduleName' => $module)
);

View File

@ -0,0 +1,97 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
use \Zend_Controller_Action_Exception;
use Icinga\Application\Icinga;
use Icinga\Module\Doc\DocController;
use Icinga\Module\Doc\DocParser;
use Icinga\Module\Doc\Exception\DocException;
use Icinga\Module\Doc\Search\DocSearch;
use Icinga\Module\Doc\Search\DocSearchIterator;
use Icinga\Module\Doc\Renderer\DocSearchRenderer;
class Doc_SearchController extends DocController
{
/**
* Render search
*/
public function indexAction()
{
$parser = new DocParser($this->getWebPath());
$search = new DocSearchRenderer(
new DocSearchIterator(
$parser->getDocTree()->getIterator(),
new DocSearch($this->params->get('q'))
)
);
$search->setUrl('doc/icingaweb/chapter');
$searches = array(
'Icinga Web 2' => $search
);
foreach (Icinga::app()->getModuleManager()->listEnabledModules() as $module) {
if (($path = $this->getModulePath($module)) !== null) {
try {
$parser = new DocParser($path);
} catch (DocException $e) {
continue;
}
$search = new DocSearchRenderer(
new DocSearchIterator(
$parser->getDocTree()->getIterator(),
new DocSearch($this->params->get('q'))
)
);
$search
->setUrl('doc/module/chapter')
->setUrlParams(array('moduleName' => $module));
$searches[$module] = $search;
}
}
$this->view->searches = $searches;
}
/**
* Get the path to a module's documentation
*
* @param string $module
*
* @return string|null
*/
protected function getModulePath($module)
{
if (is_dir(($path = Icinga::app()->getModuleManager()->getModuleDir($module, '/doc')))) {
return $path;
}
if (($path = $this->Config()->get('documentation', 'modules')) !== null) {
$path = str_replace('{module}', $module, $path);
if (is_dir($path)) {
return $path;
}
}
return null;
}
/**
* Get the path to Icinga Web 2's documentation
*
* @return string
*
* @throws Zend_Controller_Action_Exception If Icinga Web 2's documentation is not available
*/
protected function getWebPath()
{
$path = Icinga::app()->getBaseDir('doc');
if (is_dir($path)) {
return $path;
}
if (($path = $this->Config()->get('documentation', 'icingaweb2')) !== null) {
if (is_dir($path)) {
return $path;
}
}
throw new Zend_Controller_Action_Exception(
$this->translate('Documentation for Icinga Web 2 is not available'),
404
);
}
}

View File

@ -1,3 +1,6 @@
<div class="chapter">
<?= $sectionRenderer->render($this, $this->getHelper('Url')); ?>
<div class="controls">
<?= /** @type \Icinga\Web\Widget\Tabs $tabs */ $tabs->showOnlyCloseButton() ?>
</div>
<div class="content">
<?= /** @type \Icinga\Module\Doc\Renderer\DocSectionRenderer $section */ $section ?>
</div>

View File

@ -1,6 +1,9 @@
<div class="controls"></div>
<h1><?= $this->translate('Available documentations'); ?></h1>
<ul>
<li><a href="<?= $this->href('doc/icingaweb/toc'); ?>">Icinga Web 2</a></li>
<li><a href="<?= $this->href('doc/module/'); ?>"><?= $this->translate('Module documentations'); ?></a></li>
</ul>
<?= /** @type \Icinga\Web\Widget\Tabs $tabs */ $tabs->showOnlyCloseButton() ?>
<h1><?= $this->translate('Available documentations') ?></h1>
<div class="content">
<ul>
<li><a href="<?= $this->href('doc/icingaweb/toc') ?>">Icinga Web 2</a></li>
<li><a href="<?= $this->href('doc/module/') ?>"><?= $this->translate('Module documentations') ?></a></li>
</ul>
</div>

View File

@ -1,3 +0,0 @@
<div class="chapter">
<?= $sectionRenderer->render($this, $this->getHelper('Url')); ?>
</div>

View File

@ -1,10 +1,15 @@
<h1><?= $this->translate('Module documentations'); ?></h1>
<ul>
<?php foreach ($modules as $module): ?>
<li>
<a href="<?= $this->getHelper('Url')->url(array('moduleName' => $module), 'doc/module/toc', false, false); ?>">
<?= $module ?>
</a>
</li>
<?php endforeach ?>
</ul>
<div class="controls">
<?= /** @type \Icinga\Web\Widget\Tabs $tabs */ $tabs->showOnlyCloseButton() ?>
<h1><?= $this->translate('Module documentations') ?></h1>
</div>
<div class="content">
<ul>
<?php foreach ($modules as $module): ?>
<li>
<a href="<?= $this->getHelper('Url')->url(array('moduleName' => $module), 'doc/module/toc', false, false) ?>">
<?= $module ?>
</a>
</li>
<?php endforeach ?>
</ul>
</div>

View File

@ -1,6 +0,0 @@
<div class="controls">
<h1><?= $title ?></h1>
</div>
<div class="content toc">
<?= $tocRenderer->render($this, $this->getHelper('Url')); ?>
</div>

View File

@ -1,7 +1,5 @@
<h1><?= $docName ?> <?= $this->translate('Documentation'); ?></h1>
<div class="toc">
<?= $tocRenderer->render($this, $this->getHelper('Url')); ?>
</div>
<div class="chapter">
<?= $sectionRenderer->render($this, $this->getHelper('Url')); ?>
<div class="content">
<h1><?= /** @type string $title */ $title ?></h1>
<?= /** @type \Icinga\Module\Doc\Renderer\DocTocRenderer $toc */ $toc ?>
<?= /** @type \Icinga\Module\Doc\Renderer\DocSectionRenderer $section */ $section ?>
</div>

View File

@ -0,0 +1,8 @@
<div class="content">
<?php foreach (/** @type \Icinga\Module\Doc\Renderer\DocSearchRenderer[] $searches */ $searches as $title => $search): ?>
<?php if (! $search->isEmpty()): ?>
<h1><?= $this->escape($title) ?></h1>
<?= $search ?>
<?php endif ?>
<?php endforeach ?>
</div>

View File

@ -1,6 +1,7 @@
<div class="controls">
<h1><?= $title ?></h1>
<?= /** @type \Icinga\Web\Widget\Tabs $tabs */ $tabs->showOnlyCloseButton() ?>
<h1><?= /** @type string $title */ $title ?></h1>
</div>
<div class="content toc">
<?= $tocRenderer->render($this, $this->getHelper('Url')); ?>
<div class="content">
<?= /** @type \Icinga\Module\Doc\Renderer\DocTocRenderer $toc */ $toc ?>
</div>

View File

@ -20,3 +20,5 @@ $section->add($this->translate('Developer - Style'), array(
'url' => 'doc/style/guide',
'priority' => 200,
));
$this->provideSearchUrl($this->translate('Doc'), 'doc/search');

View File

@ -3,6 +3,8 @@
namespace Icinga\Module\Doc;
use Icinga\Module\Doc\Renderer\DocSectionRenderer;
use Icinga\Module\Doc\Renderer\DocTocRenderer;
use Icinga\Web\Controller\ModuleActionController;
class DocController extends ModuleActionController
@ -10,40 +12,39 @@ class DocController extends ModuleActionController
/**
* Render a chapter
*
* @param string $path Path to the documentation
* @param string $chapterId ID of the chapter
* @param string $tocUrl
* @param string $url
* @param array $urlParams
* @param string $path Path to the documentation
* @param string $chapter ID of the chapter
* @param string $url URL to replace links with
* @param array $urlParams Additional URL parameters
*/
protected function renderChapter($path, $chapterId, $tocUrl, $url, array $urlParams = array())
protected function renderChapter($path, $chapter, $url, array $urlParams = array())
{
$parser = new DocParser($path);
$this->view->sectionRenderer = new SectionRenderer(
$parser->getDocTree(),
SectionRenderer::decodeUrlParam($chapterId),
$tocUrl,
$url,
$urlParams
);
$this->view->title = $chapterId;
$section = new DocSectionRenderer($parser->getDocTree(), DocSectionRenderer::decodeUrlParam($chapter));
$this->view->section = $section
->setUrl($url)
->setUrlParams($urlParams)
->setHighlightSearch($this->params->get('highlight-search'));
$this->view->title = $chapter;
$this->render('chapter', null, true);
}
/**
* Render a toc
*
* @param string $path Path to the documentation
* @param string $name Name of the documentation
* @param string $url
* @param array $urlParams
* @param string $path Path to the documentation
* @param string $name Name of the documentation
* @param string $url URL to replace links with
* @param array $urlParams Additional URL parameters
*/
protected function renderToc($path, $name, $url, array $urlParams = array())
{
$parser = new DocParser($path);
$this->view->tocRenderer = new TocRenderer($parser->getDocTree(), $url, $urlParams);
$toc = new DocTocRenderer($parser->getDocTree()->getIterator());
$this->view->toc = $toc
->setUrl($url)
->setUrlParams($urlParams);
$name = ucfirst($name);
$this->view->docName = $name;
$this->view->title = sprintf($this->translate('%s Documentation'), $name);
$this->render('toc', null, true);
}
@ -59,17 +60,16 @@ class DocController extends ModuleActionController
protected function renderPdf($path, $name, $url, array $urlParams = array())
{
$parser = new DocParser($path);
$docTree = $parser->getDocTree();
$this->view->tocRenderer = new TocRenderer($docTree, $url, $urlParams);
$this->view->sectionRenderer = new SectionRenderer(
$docTree,
null,
null,
$url,
$urlParams
);
$this->view->docName = $name;
$toc = new DocTocRenderer($parser->getDocTree()->getIterator());
$this->view->toc = $toc
->setUrl($url)
->setUrlParams($urlParams);
$section = new DocSectionRenderer($parser->getDocTree());
$this->view->section = $section
->setUrl($url)
->setUrlParams($urlParams);
$this->view->title = sprintf($this->translate('%s Documentation'), $name);
$this->_request->setParam('format', 'pdf');
$this->render('pdf', null, true);
$this->_helper->viewRenderer->setRender('pdf', null, true);
}
}

View File

@ -6,8 +6,8 @@ namespace Icinga\Module\Doc;
use ArrayIterator;
use Countable;
use IteratorAggregate;
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Icinga\File\NonEmptyFileIterator;
use Icinga\File\FileExtensionFilterIterator;
@ -19,7 +19,7 @@ class DocIterator implements Countable, IteratorAggregate
/**
* Ordered files
*
* @var array
* @type array
*/
protected $fileInfo;
@ -46,8 +46,7 @@ class DocIterator implements Countable, IteratorAggregate
}
/**
* (non-PHPDoc)
* @see Countable::count()
* {@inheritdoc}
*/
public function count()
{
@ -55,8 +54,7 @@ class DocIterator implements Countable, IteratorAggregate
}
/**
* (non-PHPDoc)
* @see IteratorAggregate::getIterator()
* {@inheritdoc}
*/
public function getIterator()
{

View File

@ -3,7 +3,9 @@
namespace Icinga\Module\Doc;
use SplDoublyLinkedList;
use LogicException;
use SplStack;
use Icinga\Data\Tree\SimpleTree;
use Icinga\Exception\NotReadableError;
use Icinga\Module\Doc\Exception\DocEmptyException;
use Icinga\Module\Doc\Exception\DocException;
@ -16,14 +18,14 @@ class DocParser
/**
* Path to the documentation
*
* @var string
* @type string
*/
protected $path;
/**
* Iterator over documentation files
*
* @var DocIterator
* @type DocIterator
*/
protected $docIterator;
@ -121,16 +123,15 @@ class DocParser
/**
* Get the documentation tree
*
* @return DocTree
* @return SimpleTree
*/
public function getDocTree()
{
$tree = new DocTree();
$stack = new SplDoublyLinkedList();
$tree = new SimpleTree();
$stack = new SplStack();
foreach ($this->docIterator as $fileInfo) {
/* @var $file \SplFileInfo */
/** @type $fileInfo \SplFileInfo */
$file = $fileInfo->openFile();
/* @var $file \SplFileObject */
$lastLine = null;
foreach ($file as $line) {
$header = $this->extractHeader($line, $lastLine);
@ -142,7 +143,7 @@ class DocParser
if ($id === null) {
$path = array();
foreach ($stack as $section) {
/* @var $section Section */
/** @type $section DocSection */
$path[] = $section->getTitle();
}
$path[] = $title;
@ -151,17 +152,27 @@ class DocParser
} else {
$noFollow = false;
}
if ($tree->getNode($id) !== null) {
$id = uniqid($id);
}
$section = new DocSection();
$section
->setId($id)
->setTitle($title)
->setLevel($level)
->setNoFollow($noFollow);
if ($stack->isEmpty()) {
$chapterId = $id;
$section = new Section($id, $title, $level, $noFollow, $chapterId);
$tree->addRoot($section);
$section->setChapter($section);
$tree->addChild($section);
} else {
$chapterId = $stack->bottom()->getId();
$section = new Section($id, $title, $level, $noFollow, $chapterId);
$section->setChapter($stack->bottom());
$tree->addChild($section, $stack->top());
}
$stack->push($section);
} else {
if ($stack->isEmpty()) {
throw new LogicException('Heading required');
}
$stack->top()->appendContent($line);
}
// Save last line for setext-style headers

View File

@ -0,0 +1,159 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Doc;
use Icinga\Data\Tree\TreeNode;
/**
* A section of a documentation
*/
class DocSection extends TreeNode
{
/**
* Chapter the section belongs to
*
* @type DocSection
*/
protected $chapter;
/**
* Content of the section
*
* @type array
*/
protected $content = array();
/**
* Header level
*
* @type int
*/
protected $level;
/**
* Whether to instruct search engines to not index the link to the section
*
* @type bool
*/
protected $noFollow;
/**
* Title of the section
*
* @type string
*/
protected $title;
/**
* Set the chapter the section belongs to
*
* @param DocSection $section
*
* @return $this
*/
public function setChapter(DocSection $section)
{
$this->chapter = $section;
return $this;
}
/**
* Get the chapter the section belongs to
*
* @return DocSection
*/
public function getChapter()
{
return $this->chapter;
}
/**
* Append content
*
* @param string $content
*/
public function appendContent($content)
{
$this->content[] = $content;
}
/**
* Get the content of the section
*
* @return array
*/
public function getContent()
{
return $this->content;
}
/**
* Set the header level
*
* @param int $level Header level
*
* @return $this
*/
public function setLevel($level)
{
$this->level = (int) $level;
return $this;
}
/**
* Get the header level
*
* @return int
*/
public function getLevel()
{
return $this->level;
}
/**
* Set whether to instruct search engines to not index the link to the section
*
* @param bool $noFollow Whether to instruct search engines to not index the link to the section
*
* @return $this
*/
public function setNoFollow($noFollow = true)
{
$this->noFollow = (bool) $noFollow;
return $this;
}
/**
* Get whether to instruct search engines to not index the link to the section
*
* @return bool
*/
public function getNoFollow()
{
return $this->noFollow;
}
/**
* Set the title of the section
*
* @param string $title Title of the section
*
* @return $this
*/
public function setTitle($title)
{
$this->title = (string) $title;
return $this;
}
/**
* Get the title of the section
*
* @return string
*/
public function getTitle()
{
return $this->title;
}
}

View File

@ -0,0 +1,79 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Doc;
use Countable;
use RecursiveFilterIterator;
use Icinga\Data\Tree\TreeNodeIterator;
/**
* Recursive filter iterator over sections that are part of a particular chapter
*
* @method TreeNodeIterator getInnerIterator() {
* {@inheritdoc}
* }
*/
class DocSectionFilterIterator extends RecursiveFilterIterator implements Countable
{
/**
* Chapter to filter for
*
* @type string
*/
protected $chapter;
/**
* Create a new recursive filter iterator over sections that are part of a particular chapter
*
* @param TreeNodeIterator $iterator
* @param string $chapter The chapter to filter for
*/
public function __construct(TreeNodeIterator $iterator, $chapter)
{
parent::__construct($iterator);
$this->chapter = $chapter;
}
/**
* Accept sections that are part of the given chapter
*
* @return bool Whether the current element of the iterator is acceptable
* through this filter
*/
public function accept()
{
$section = $this->current();
/** @type \Icinga\Module\Doc\DocSection $section */
if ($section->getChapter()->getId() === $this->chapter) {
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function getChildren()
{
return new static($this->getInnerIterator()->getChildren(), $this->chapter);
}
/**
* {@inheritdoc}
*/
public function count()
{
return iterator_count($this);
}
/**
* Whether the filter swallowed every section
*
* @return bool
*/
public function isEmpty()
{
return $this->count() === 0;
}
}

View File

@ -1,79 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Doc;
use LogicException;
use Icinga\Data\Identifiable;
use Icinga\Data\Tree\Node;
/**
* Documentation tree
*/
class DocTree extends Node
{
/**
* All nodes of the tree
*
* @var array
*/
protected $nodes = array();
/**
* Append a root node to the tree
*
* @param Identifiable $root
*/
public function addRoot(Identifiable $root)
{
$rootId = $root->getId();
if (isset($this->nodes[$rootId])) {
$rootId = uniqid($rootId);
// throw new LogicException(
// sprintf('Can\'t add root node: a root node with the id \'%s\' already exists', $rootId)
// );
}
$this->nodes[$rootId] = $this->appendChild($root);
}
/**
* Append a child node to a parent node
*
* @param Identifiable $child
* @param Identifiable $parent
*
* @throws LogicException If the the tree does not contain the parent node
*/
public function addChild(Identifiable $child, Identifiable $parent)
{
$childId = $child->getId();
$parentId = $parent->getId();
if (isset($this->nodes[$childId])) {
$childId = uniqid($childId);
// throw new LogicException(
// sprintf('Can\'t add child node: a child node with the id \'%s\' already exists', $childId)
// );
}
if (! isset($this->nodes[$parentId])) {
throw new LogicException(
sprintf(mt('doc', 'Can\'t add child node: there\'s no parent node having the id \'%s\''), $parentId)
);
}
$this->nodes[$childId] = $this->nodes[$parentId]->appendChild($child);
}
/**
* Get a node
*
* @param mixed $id
*
* @return Node|null
*/
public function getNode($id)
{
if (! isset($this->nodes[$id])) {
return null;
}
return $this->nodes[$id];
}
}

View File

@ -6,4 +6,6 @@ namespace Icinga\Module\Doc\Exception;
/**
* Exception thrown if a chapter was not found
*/
class ChapterNotFoundException extends DocException {}
class ChapterNotFoundException extends DocException
{
}

View File

@ -6,4 +6,6 @@ namespace Icinga\Module\Doc\Exception;
/**
* Exception thrown if a documentation directory is empty
*/
class DocEmptyException extends DocException {}
class DocEmptyException extends DocException
{
}

View File

@ -3,9 +3,11 @@
namespace Icinga\Module\Doc\Exception;
use RuntimeException;
use Icinga\Exception\IcingaException;
/**
* Exception thrown if an error in the documentation module's library occurs
*/
class DocException extends RuntimeException {}
class DocException extends IcingaException
{
}

View File

@ -1,74 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Doc;
use RecursiveIteratorIterator;
use Zend_View_Helper_Url;
use Icinga\Web\View;
/**
* Base class for toc and section renderer
*/
abstract class Renderer extends RecursiveIteratorIterator
{
/**
* Encode an anchor identifier
*
* @param string $anchor
*
* @return string
*/
public static function encodeAnchor($anchor)
{
return rawurlencode($anchor);
}
/**
* Decode an anchor identifier
*
* @param string $anchor
*
* @return string
*/
public static function decodeAnchor($anchor)
{
return rawurldecode($anchor);
}
/**
* Encode a URL parameter
*
* @param string $param
*
* @return string
*/
public static function encodeUrlParam($param)
{
return str_replace(array('%2F','%5C'), array('%252F','%255C'), rawurlencode($param));
}
/**
* Decode a URL parameter
*
* @param string $param
*
* @return string
*/
public static function decodeUrlParam($param)
{
return str_replace(array('%2F', '%5C'), array('/', '\\'), $param);
}
/**
* Render to HTML
*
* Meant to be overwritten by concrete classes.
*
* @param View $view
* @param Zend_View_Helper_Url $zendUrlHelper
*
* @return string
*/
abstract public function render(View $view, Zend_View_Helper_Url $zendUrlHelper);
}

View File

@ -0,0 +1,178 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Doc\Renderer;
use Exception;
use RecursiveIteratorIterator;
use Icinga\Application\Icinga;
use Icinga\Web\View;
/**
* Base class for toc and section renderer
*/
abstract class DocRenderer extends RecursiveIteratorIterator
{
/**
* URL to replace links with
*
* @type string
*/
protected $url;
/**
* Additional URL parameters
*
* @type array
*/
protected $urlParams = array();
/**
* View
*
* @type View|null
*/
protected $view;
/**
* Set the URL to replace links with
*
* @param string $url
*
* @return $this
*/
public function setUrl($url)
{
$this->url = (string) $url;
return $this;
}
/**
* Get the URL to replace links with
*
* @return string
*/
public function getUrl()
{
return $this->url;
}
/**
* Set additional URL parameters
*
* @param array $urlParams
*
* @return $this
*/
public function setUrlParams(array $urlParams)
{
$this->urlParams = array_map(array($this, 'encodeUrlParam'), $urlParams);
return $this;
}
/**
* Get additional URL parameters
*
* @return array
*/
public function getUrlParams()
{
return $this->urlParams;
}
/**
* Set the view
*
* @param View $view
*
* @return $this
*/
public function setView(View $view)
{
$this->view = $view;
return $this;
}
/**
* Get the view
*
* @return View
*/
public function getView()
{
if ($this->view === null) {
$this->view = Icinga::app()->getViewRenderer()->view;
}
return $this->view;
}
/**
* Encode an anchor identifier
*
* @param string $anchor
*
* @return string
*/
public static function encodeAnchor($anchor)
{
return rawurlencode($anchor);
}
/**
* Decode an anchor identifier
*
* @param string $anchor
*
* @return string
*/
public static function decodeAnchor($anchor)
{
return rawurldecode($anchor);
}
/**
* Encode a URL parameter
*
* @param string $param
*
* @return string
*/
public static function encodeUrlParam($param)
{
return str_replace(array('%2F','%5C'), array('%252F','%255C'), rawurlencode($param));
}
/**
* Decode a URL parameter
*
* @param string $param
*
* @return string
*/
public static function decodeUrlParam($param)
{
return str_replace(array('%2F', '%5C'), array('/', '\\'), $param);
}
/**
* Render to HTML
*
* @return string
*/
abstract public function render();
/**
* Render to HTML
*
* @return string
* @see \Icinga\Module\Doc\Renderer::render() For the render method.
*/
public function __toString()
{
try {
return $this->render();
} catch (Exception $e) {
return $e->getMessage() . ': ' . $e->getTraceAsString();
}
}
}

View File

@ -0,0 +1,125 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Doc\Renderer;
use RecursiveIteratorIterator;
use Icinga\Module\Doc\Search\DocSearchIterator;
use Icinga\Module\Doc\Search\DocSearchMatch;
/**
* Renderer for doc searches
*
* @method DocSearchIterator getInnerIterator() {
* @{inheritdoc}
* }
*/
class DocSearchRenderer extends DocRenderer
{
/**
* The content to render
*
* @type array
*/
protected $content = array();
/**
* Create a new renderer for doc searches
*
* @param DocSearchIterator $iterator
*/
public function __construct (DocSearchIterator $iterator)
{
parent::__construct($iterator, RecursiveIteratorIterator::SELF_FIRST);
}
/**
* {@inheritdoc}
*/
public function beginIteration()
{
$this->content[] = '<nav role="navigation"><ul class="toc">';
}
/**
* {@inheritdoc}
*/
public function endIteration()
{
$this->content[] = '</ul></nav>';
}
/**
* {@inheritdoc}
*/
public function beginChildren()
{
if ($this->getInnerIterator()->getMatches()) {
$this->content[] = '<ul class="toc">';
}
}
/**
* {@inheritdoc}
*/
public function endChildren()
{
if ($this->getInnerIterator()->getMatches()) {
$this->content[] = '</ul>';
}
}
/**
* {@inheritdoc}
*/
public function render()
{
foreach ($this as $section) {
if (($matches = $this->getInnerIterator()->getMatches()) === null) {
continue;
}
$title = $this->getView()->escape($section->getTitle());
$contentMatches = array();
foreach ($matches as $match) {
if ($match->getMatchType() === DocSearchMatch::MATCH_HEADER) {
$title = $match->highlight();
} else {
$contentMatches[] = sprintf(
'<p>%s</p>',
$match->highlight()
);
}
}
$path = $this->getView()->getHelper('Url')->url(
array_merge(
$this->getUrlParams(),
array(
'chapter' => $this->encodeUrlParam($section->getChapter()->getId())
)
),
$this->url,
false,
false
);
$url = $this->getView()->url(
$path,
array('highlight-search' => $this->getInnerIterator()->getSearch()->getInput())
);
/** @type \Icinga\Web\Url $url */
$url->setAnchor($this->encodeAnchor($section->getId()));
$this->content[] = sprintf(
'<li><a data-base-target="_next" %shref="%s">%s</a>',
$section->getNoFollow() ? 'rel="nofollow" ' : '',
$url->getAbsoluteUrl(),
$title
);
if (! empty($contentMatches)) {
$this->content = array_merge($this->content, $contentMatches);
}
if (! $section->hasChildren()) {
$this->content[] = '</li>';
}
}
return implode("\n", $this->content);
}
}

View File

@ -0,0 +1,285 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Doc\Renderer;
require_once 'Parsedown/Parsedown.php';
use DOMDocument;
use DOMXPath;
use Parsedown;
use RecursiveIteratorIterator;
use Icinga\Data\Tree\SimpleTree;
use Icinga\Module\Doc\Exception\ChapterNotFoundException;
use Icinga\Module\Doc\DocSectionFilterIterator;
use Icinga\Module\Doc\Search\DocSearch;
use Icinga\Module\Doc\Search\DocSearchMatch;
use Icinga\Web\Dom\DomNodeIterator;
use Icinga\Web\Url;
use Icinga\Web\View;
/**
* Section renderer
*/
class DocSectionRenderer extends DocRenderer
{
/**
* Content to render
*
* @type array
*/
protected $content = array();
/**
* Search criteria to highlight
*
* @type string
*/
protected $highlightSearch;
/**
* Parsedown instance
*
* @type Parsedown
*/
protected $parsedown;
/**
* Documentation tree
*
* @type SimpleTree
*/
protected $tree;
/**
* Create a new section renderer
*
* @param SimpleTree $tree The documentation tree
* @param string|null $chapter If not null, the chapter to filter for
*
* @throws ChapterNotFoundException If the chapter to filter for was not found
*/
public function __construct(SimpleTree $tree, $chapter = null)
{
if ($chapter !== null) {
$filter = new DocSectionFilterIterator($tree->getIterator(), $chapter);
if ($filter->isEmpty()) {
throw new ChapterNotFoundException(
mt('doc', 'Chapter %s not found'), $chapter
);
}
parent::__construct(
$filter,
RecursiveIteratorIterator::SELF_FIRST
);
} else {
parent::__construct($tree->getIterator(), RecursiveIteratorIterator::SELF_FIRST);
}
$this->tree = $tree;
$this->parsedown = Parsedown::instance();
}
/**
* Set the search criteria to highlight
*
* @param string $highlightSearch
*
* @return $this
*/
public function setHighlightSearch($highlightSearch)
{
$this->highlightSearch = $highlightSearch;
return $this;
}
/**
* Get the search criteria to highlight
*
* @return string
*/
public function getHighlightSearch()
{
return $this->highlightSearch;
}
/**
* Syntax highlighting for PHP code
*
* @param array $match
*
* @return string
*/
protected function highlightPhp($match)
{
return '<pre>' . highlight_string(htmlspecialchars_decode($match[1]), true) . '</pre>';
}
/**
* Highlight search criteria
*
* @param string $html
* @param DocSearch $search Search criteria
*
* @return string
*/
protected function highlightSearch($html, DocSearch $search)
{
$doc = new DOMDocument();
@$doc->loadHTML($html);
$iter = new RecursiveIteratorIterator(new DomNodeIterator($doc), RecursiveIteratorIterator::SELF_FIRST);
foreach ($iter as $node) {
if ($node->nodeType !== XML_TEXT_NODE
|| ($node->parentNode->nodeType === XML_ELEMENT_NODE && $node->parentNode->tagName === 'code')
) {
continue;
}
$text = $node->nodeValue;
if (($match = $search->search($text)) === null) {
continue;
}
$matches = $match->getMatches();
ksort($matches);
$offset = 0;
$fragment = $doc->createDocumentFragment();
foreach ($matches as $position => $match) {
$fragment->appendChild($doc->createTextNode(substr($text, $offset, $position - $offset)));
$fragment->appendChild($doc->createElement('span', $match))
->setAttribute('class', DocSearchMatch::HIGHLIGHT_CSS_CLASS);
$offset = $position + strlen($match);
}
$fragment->appendChild($doc->createTextNode(substr($text, $offset)));
$node->parentNode->replaceChild($fragment, $node);
}
// Remove <!DOCTYPE
$doc->removeChild($doc->doctype);
// Remove <html><body> and </body></html>
return substr($doc->saveHTML(), 12, -15);
}
/**
* Markup notes
*
* @param array $match
*
* @return string
*/
protected function markupNotes($match)
{
$doc = new DOMDocument();
$doc->loadHTML($match[0]);
$xpath = new DOMXPath($doc);
$blockquote = $xpath->query('//blockquote[1]')->item(0);
/** @type \DOMElement $blockquote */
if (strtolower(substr(trim($blockquote->nodeValue), 0, 5)) === 'note:') {
$blockquote->setAttribute('class', 'note');
}
return $doc->saveXML($blockquote);
}
/**
* Replace img src tags
*
* @param $match
*
* @return string
*/
protected function replaceImg($match)
{
$doc = new DOMDocument();
$doc->loadHTML($match[0]);
$xpath = new DOMXPath($doc);
$img = $xpath->query('//img[1]')->item(0);
/** @type \DOMElement $img */
$img->setAttribute('src', Url::fromPath($img->getAttribute('src'))->getAbsoluteUrl());
return substr_replace($doc->saveXML($img), '', -2, 1); // Replace '/>' with '>'
}
/**
* Replace link
*
* @param array $match
*
* @return string
*/
protected function replaceLink($match)
{
if (($section = $this->tree->getNode($this->decodeAnchor($match['fragment']))) === null) {
return $match[0];
}
/** @type \Icinga\Module\Doc\DocSection $section */
$path = $this->getView()->getHelper('Url')->url(
array_merge(
$this->urlParams,
array(
'chapter' => $this->encodeUrlParam($section->getChapter()->getId())
)
),
$this->url,
false,
false
);
$url = $this->getView()->url($path);
/** @type \Icinga\Web\Url $url */
$url->setAnchor($this->encodeAnchor($section->getId()));
return sprintf(
'<a %s%shref="%s"',
strlen($match['attribs']) ? trim($match['attribs']) . ' ' : '',
$section->getNoFollow() ? 'rel="nofollow" ' : '',
$url->getAbsoluteUrl()
);
}
/**
* {@inheritdoc}
*/
public function render()
{
$search = null;
if (($highlightSearch = $this->getHighlightSearch()) !== null) {
$search = new DocSearch($highlightSearch);
}
foreach ($this as $section) {
$title = $section->getTitle();
if ($search !== null && ($match = $search->search($title)) !== null) {
$title = $match->highlight();
} else {
$title = $this->getView()->escape($title);
}
$this->content[] = sprintf(
'<a name="%1$s"></a><h%2$d>%3$s</h%2$d>',
static::encodeAnchor($section->getId()),
$section->getLevel(),
$title
);
$html = $this->parsedown->text(implode('', $section->getContent()));
if (empty($html)) {
continue;
}
$html = preg_replace_callback(
'#<pre><code class="language-php">(.*?)</code></pre>#s',
array($this, 'highlightPhp'),
$html
);
$html = preg_replace_callback(
'/<img[^>]+>/',
array($this, 'replaceImg'),
$html
);
$html = preg_replace_callback(
'#<blockquote>.+</blockquote>#ms',
array($this, 'markupNotes'),
$html
);
$html = preg_replace_callback(
'/<a\s+(?P<attribs>[^>]*?\s+)?href="(?:(?!http:\/\/)[^"#]*)#(?P<fragment>[^"]+)"/',
array($this, 'replaceLink'),
$html
);
if ($search !== null) {
$html = $this->highlightSearch($html, $search);
}
$this->content[] = $html;
}
return implode("\n", $this->content);
}
}

View File

@ -0,0 +1,102 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Doc\Renderer;
use Icinga\Web\View;
use Icinga\Data\Tree\TreeNodeIterator;
use RecursiveIteratorIterator;
/**
* TOC renderer
*
* @method TreeNodeIterator getInnerIterator() {
* {@inheritdoc}
* }
*/
class DocTocRenderer extends DocRenderer
{
/**
* Content to render
*
* @type array
*/
protected $content = array();
/**
* Create a new toc renderer
*
* @param TreeNodeIterator $iterator
*/
public function __construct(TreeNodeIterator $iterator)
{
parent::__construct($iterator, RecursiveIteratorIterator::SELF_FIRST);
}
/**
* {@inheritdoc}
*/
public function beginIteration()
{
$this->content[] = '<nav role="navigation"><ul class="toc">';
}
/**
* {@inheritdoc}
*/
public function endIteration()
{
$this->content[] = '</ul></nav>';
}
/**
* {@inheritdoc}
*/
public function beginChildren()
{
$this->content[] = '<ul class="toc">';
}
/**
* {@inheritdoc}
*/
public function endChildren()
{
$this->content[] = '</ul>';
}
/**
* {@inheritdoc}
*/
public function render()
{
$view = $this->getView();
$zendUrlHelper = $view->getHelper('Url');
foreach ($this as $section) {
$path = $zendUrlHelper->url(
array_merge(
$this->urlParams,
array(
'chapter' => $this->encodeUrlParam($section->getChapter()->getId())
)
),
$this->url,
false,
false
);
$url = $view->url($path);
/** @type \Icinga\Web\Url $url */
$url->setAnchor($this->encodeAnchor($section->getId()));
$this->content[] = sprintf(
'<li><a data-base-target="_next" %shref="%s">%s</a>',
$section->getNoFollow() ? 'rel="nofollow" ' : '',
$url->getAbsoluteUrl(),
$view->escape($section->getTitle())
);
if (! $section->hasChildren()) {
$this->content[] = '</li>';
}
}
return implode("\n", $this->content);
}
}

View File

@ -0,0 +1,95 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Doc\Search;
/**
* Search documentation for a given search string
*/
class DocSearch
{
/**
* Search string
*
* @type string
*/
protected $input;
/**
* Search criteria
*
* @type array
*/
protected $search;
/**
* Create a new doc search from the given search string
*
* @param string $search
*/
public function __construct($search)
{
$this->input = $search = (string) $search;
$criteria = array();
if (preg_match_all('/"(?P<search>[^"]*)"/', $search, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
$unquoted = array();
$offset = 0;
foreach ($matches as $match) {
$fullMatch = $match[0];
$searchMatch = $match['search'];
$unquoted[] = substr($search, $offset, $fullMatch[1] - $offset);
$offset = $fullMatch[1] + strlen($fullMatch[0]);
if (strlen($searchMatch[0]) > 0) {
$criteria[] = $searchMatch[0];
}
}
$unquoted[] = substr($search, $offset);
$search = implode(' ', $unquoted);
}
$this->search = array_map(
'strtolower',
array_unique(array_merge($criteria, array_filter(explode(' ', trim($search)))))
);
}
/**
* Get the search criteria
*
* @return array
*/
public function getCriteria()
{
return $this->search;
}
/**
* Get the search string
*
* @return string
*/
public function getInput()
{
return $this->input;
}
/**
* Search in the given line
*
* @param string $line
*
* @return DocSearchMatch|null
*/
public function search($line)
{
$match = new DocSearchMatch();
$match->setLine($line);
foreach ($this->search as $criteria) {
$offset = 0;
while (($position = stripos($line, $criteria, $offset)) !== false) {
$match->appendMatch(substr($line, $position, strlen($criteria)), $position);
$offset = $position + 1;
}
}
return $match->isEmpty() ? null : $match;
}
}

View File

@ -0,0 +1,120 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Doc\Search;
use RecursiveFilterIterator;
use RecursiveIteratorIterator;
use Icinga\Data\Tree\TreeNodeIterator;
/**
* Iterator over doc sections that match a given search criteria
*
* @method TreeNodeIterator getInnerIterator() {
* {@inheritdoc}
* }
*/
class DocSearchIterator extends RecursiveFilterIterator
{
/**
* Search criteria
*
* @type DocSearch
*/
protected $search;
/**
* Current search matches
*
* @type DocSearchMatch[]|null
*/
protected $matches;
/**
* Create a new iterator over doc sections that match the given search criteria
*
* @param TreeNodeIterator $iterator
* @param DocSearch $search
*/
public function __construct(TreeNodeIterator $iterator, DocSearch $search)
{
$this->search = $search;
parent::__construct($iterator);
}
/**
* Accept sections that match the search
*
* @return bool Whether the current element of the iterator is acceptable
* through this filter
*/
public function accept()
{
$section = $this->current();
/** @type $section \Icinga\Module\Doc\DocSection */
$matches = array();
if (($match = $this->search->search($section->getTitle())) !== null) {
$matches[] = $match->setMatchType(DocSearchMatch::MATCH_HEADER);
}
foreach ($section->getContent() as $lineno => $line) {
if (($match = $this->search->search($line)) !== null) {
$matches[] = $match
->setMatchType(DocSearchMatch::MATCH_CONTENT)
->setLineno($lineno);
}
}
if (! empty($matches)) {
$this->matches = $matches;
return $this;
}
if ($section->hasChildren()) {
$this->matches = null;
return true;
}
return false;
}
/**
* Get the search criteria
*
* @return DocSearch
*/
public function getSearch()
{
return $this->search;
}
/**
* {@inheritdoc}
*/
public function getChildren()
{
return new static($this->getInnerIterator()->getChildren(), $this->search);
}
/**
* Whether the search did not yield any match
*
* @return bool
*/
public function isEmpty()
{
$iter = new RecursiveIteratorIterator($this, RecursiveIteratorIterator::SELF_FIRST);
foreach ($iter as $section) {
if ($iter->getInnerIterator()->getMatches() !== null) {
return false;
}
}
return true;
}
/**
* Get current matches
*
* @return DocSearchMatch[]|null
*/
public function getMatches()
{
return $this->matches;
}
}

View File

@ -0,0 +1,215 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Doc\Search;
use UnexpectedValueException;
use Icinga\Application\Icinga;
use Icinga\Web\View;
/**
* A doc search match
*/
class DocSearchMatch
{
/**
* CSS class for highlighting matches
*
* @type string
*/
const HIGHLIGHT_CSS_CLASS = 'search-highlight';
/**
* Header match
*
* @type int
*/
const MATCH_HEADER = 1;
/**
* Content match
*
* @type int
*/
const MATCH_CONTENT = 2;
/**
* Line
*
* @type string
*/
protected $line;
/**
* Line number
*
* @type int
*/
protected $lineno;
/**
* Type of the match
*
* @type int
*/
protected $matchType;
/**
* Matches
*
* @type array
*/
protected $matches = array();
/**
* View
*
* @type View|null
*/
protected $view;
/**
* Set the line
*
* @param string $line
*
* @return $this
*/
public function setLine($line)
{
$this->line = (string) $line;
return $this;
}
/**
* Get the line
*
* @return string
*/
public function getLine()
{
return $this->line;
}
/**
* Set the line number
*
* @param int $lineno
*
* @return $this
*/
public function setLineno($lineno)
{
$this->lineno = (int) $lineno;
return $this;
}
/**
* Set the match type
*
* @param int $matchType
*
* @return $this
*/
public function setMatchType($matchType)
{
$matchType = (int) $matchType;
if ($matchType !== static::MATCH_HEADER && $matchType !== static::MATCH_CONTENT) {
throw new UnexpectedValueException();
}
$this->matchType = $matchType;
return $this;
}
/**
* Get the match type
*
* @return int
*/
public function getMatchType()
{
return $this->matchType;
}
/**
* Append a match
*
* @param string $match
* @param int $position
*
* @return $this
*/
public function appendMatch($match, $position)
{
$this->matches[(int) $position] = (string) $match;
return $this;
}
/**
* Get the matches
*
* @return array
*/
public function getMatches()
{
return $this->matches;
}
/**
* Set the view
*
* @param View $view
*
* @return $this
*/
public function setView(View $view)
{
$this->view = $view;
return $this;
}
/**
* Get the view
*
* @return View
*/
public function getView()
{
if ($this->view === null) {
$this->view = Icinga::app()->getViewRenderer()->view;
}
return $this->view;
}
/**
* Get the line having matches highlighted
*
* @return string
*/
public function highlight()
{
$highlighted = '';
$offset = 0;
$matches = $this->getMatches();
ksort($matches);
foreach ($matches as $position => $match) {
$highlighted .= $this->getView()->escape(substr($this->line, $offset, $position - $offset))
. '<span class="' . static::HIGHLIGHT_CSS_CLASS .'">'
. $this->getView()->escape($match)
. '</span>';
$offset = $position + strlen($match);
}
$highlighted .= $this->getView()->escape(substr($this->line, $offset));
return $highlighted;
}
/**
* Whether the match is empty
*
* @return bool
*/
public function isEmpty()
{
return empty($this->matches);
}
}

View File

@ -1,142 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Doc;
use Icinga\Data\Identifiable;
/**
* A section of a documentation
*/
class Section implements Identifiable
{
/**
* The ID of the section
*
* @var string
*/
protected $id;
/**
* The title of the section
*
* @var string
*/
protected $title;
/**
* The header level
*
* @var int
*/
protected $level;
/**
* Whether to instruct search engines to not index the link to the section
*
* @var bool
*/
protected $noFollow;
/**
* The ID of the chapter the section is part of
*
* @var string
*/
protected $chapterId;
/**
* The content of the section
*
* @var array
*/
protected $content = array();
/**
* Create a new section
*
* @param string $id The ID of the section
* @param string $title The title of the section
* @param int $level The header level
* @param bool $noFollow Whether to instruct search engines to not index the link to the section
* @param string $chapterId The ID of the chapter the section is part of
*/
public function __construct($id, $title, $level, $noFollow, $chapterId)
{
$this->id = $id;
$this->title = $title;
$this->level = $level;
$this->noFollow = $noFollow;
$this->chapterId= $chapterId;
}
/**
* Get the ID of the section
*
* @return string
*/
public function getId()
{
return $this->id;
}
/**
* Get the title of the section
*
* @return string
*/
public function getTitle()
{
return $this->title;
}
/**
* Get the header level
*
* @return int
*/
public function getLevel()
{
return $this->level;
}
/**
* Whether to instruct search engines to not index the link to the section
*
* @return bool
*/
public function isNoFollow()
{
return $this->noFollow;
}
/**
* The ID of the chapter the section is part of
*
* @return string
*/
public function getChapterId()
{
return $this->chapterId;
}
/**
* Append content
*
* @param string $content
*/
public function appendContent($content)
{
$this->content[] = $content;
}
/**
* Get the content of the section
*
* @return array
*/
public function getContent()
{
return $this->content;
}
}

View File

@ -1,67 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Doc;
use Countable;
use RecursiveFilterIterator;
use Icinga\Data\Tree\NodeInterface;
/**
* Recursive iterator over sections that are part of a particular chapter
*/
class SectionFilterIterator extends RecursiveFilterIterator implements Countable
{
/**
* The chapter ID to filter for
*
* @var string
*/
protected $chapterId;
/**
* Create a new SectionFilterIterator
*
* @param NodeInterface $node Node
* @param string $chapterId The chapter ID to filter for
*/
public function __construct(NodeInterface $node, $chapterId)
{
parent::__construct($node);
$this->chapterId = $chapterId;
}
/**
* Accept sections that are part of the given chapter
*
* @return bool Whether the current element of the iterator is acceptable
* through this filter
*/
public function accept()
{
$section = $this->getInnerIterator()->current()->getValue();
/* @var $section \Icinga\Module\Doc\Section */
if ($section->getChapterId() === $this->chapterId) {
return true;
}
return false;
}
/**
* (non-PHPDoc)
* @see RecursiveFilterIterator::getChildren()
*/
public function getChildren()
{
return new static($this->getInnerIterator()->getChildren(), $this->chapterId);
}
/**
* (non-PHPDoc)
* @see Countable::count()
*/
public function count()
{
return iterator_count($this);
}
}

View File

@ -1,309 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Doc;
require_once 'Parsedown/Parsedown.php';
use DOMDocument;
use DOMXPath;
use RecursiveIteratorIterator;
use Parsedown;
use Zend_View_Helper_Url;
use Icinga\Module\Doc\Exception\ChapterNotFoundException;
use Icinga\Web\Url;
use Icinga\Web\View;
/**
* preg_replace_callback helper to replace links
*/
class Callback
{
protected $docTree;
protected $view;
protected $zendUrlHelper;
protected $url;
protected $urlParams;
public function __construct(
DocTree $docTree,
View $view,
Zend_View_Helper_Url $zendUrlHelper,
$url,
array $urlParams)
{
$this->docTree = $docTree;
$this->view = $view;
$this->zendUrlHelper = $zendUrlHelper;
$this->url = $url;
$this->urlParams = $urlParams;
}
public function render($match)
{
$node = $this->docTree->getNode(Renderer::decodeAnchor($match['fragment']));
/* @var $node \Icinga\Data\Tree\Node */
if ($node === null) {
return $match[0];
}
$section = $node->getValue();
/* @var $section \Icinga\Module\Doc\Section */
$path = $this->zendUrlHelper->url(
array_merge(
$this->urlParams,
array(
'chapterId' => SectionRenderer::encodeUrlParam($section->getChapterId())
)
),
$this->url,
false,
false
);
$url = $this->view->url($path);
$url->setAnchor(SectionRenderer::encodeAnchor($section->getId()));
return sprintf(
'<a %s%shref="%s"',
strlen($match['attribs']) ? trim($match['attribs']) . ' ' : '',
$section->isNoFollow() ? 'rel="nofollow" ' : '',
$url->getAbsoluteUrl()
);
}
}
/**
* Section renderer
*/
class SectionRenderer extends Renderer
{
/**
* The documentation tree
*
* @var DocTree
*/
protected $docTree;
protected $tocUrl;
/**
* The URL to replace links with
*
* @var string
*/
protected $url;
/**
* Additional URL parameters
*
* @var array
*/
protected $urlParams;
/**
* Parsedown instance
*
* @var Parsedown
*/
protected $parsedown;
/**
* Content
*
* @var array
*/
protected $content = array();
/**
* Create a new section renderer
*
* @param DocTree $docTree The documentation tree
* @param string|null $chapterId If not null, the chapter ID to filter for
* @param string $tocUrl
* @param string $url The URL to replace links with
* @param array $urlParams Additional URL parameters
*
* @throws ChapterNotFoundException If the chapter to filter for was not found
*/
public function __construct(DocTree $docTree, $chapterId, $tocUrl, $url, array $urlParams)
{
if ($chapterId !== null) {
$filter = new SectionFilterIterator($docTree, $chapterId);
if ($filter->count() === 0) {
throw new ChapterNotFoundException(
sprintf(mt('doc', 'Chapter \'%s\' not found'), $chapterId)
);
}
parent::__construct(
$filter,
RecursiveIteratorIterator::SELF_FIRST
);
} else {
parent::__construct($docTree, RecursiveIteratorIterator::SELF_FIRST);
}
$this->docTree = $docTree;
$this->tocUrl = $tocUrl;
$this->url = $url;
$this->urlParams = array_map(array($this, 'encodeUrlParam'), $urlParams);
$this->parsedown = Parsedown::instance();
}
/**
* Syntax highlighting for PHP code
*
* @param $match
*
* @return string
*/
protected function highlightPhp($match)
{
return '<pre>' . highlight_string(htmlspecialchars_decode($match[1]), true) . '</pre>';
}
/**
* Replace img src tags
*
* @param $match
*
* @return string
*/
protected function replaceImg($match)
{
$doc = new DOMDocument();
$doc->loadHTML($match[0]);
$xpath = new DOMXPath($doc);
$img = $xpath->query('//img[1]')->item(0);
/* @var $img \DOMElement */
$img->setAttribute('src', Url::fromPath($img->getAttribute('src'))->getAbsoluteUrl());
return substr_replace($doc->saveXML($img), '', -2, 1); // Replace '/>' with '>'
}
protected function blubb($match)
{
$doc = new DOMDocument();
$doc->loadHTML($match[0]);
$xpath = new DOMXPath($doc);
$blockquote = $xpath->query('//blockquote[1]')->item(0);
/* @var $blockquote \DOMElement */
if (strtolower(substr(trim($blockquote->nodeValue), 0, 5)) === 'note:') {
$blockquote->setAttribute('class', 'note');
}
return $doc->saveXML($blockquote);
}
/**
* Render the section
*
* @param View $view
* @param Zend_View_Helper_Url $zendUrlHelper
* @param bool $renderNavigation
*
* @return string
*/
public function render(View $view, Zend_View_Helper_Url $zendUrlHelper, $renderNavigation = true)
{
$callback = new Callback($this->docTree, $view, $zendUrlHelper, $this->url, $this->urlParams);
$content = array();
foreach ($this as $node) {
$section = $node->getValue();
/* @var $section \Icinga\Module\Doc\Section */
$content[] = sprintf(
'<a name="%1$s"></a><h%2$d>%3$s</h%2$d>',
Renderer::encodeAnchor($section->getId()),
$section->getLevel(),
$view->escape($section->getTitle())
);
$html = preg_replace_callback(
'#<pre><code class="language-php">(.*?)</code></pre>#s',
array($this, 'highlightPhp'),
$this->parsedown->text(implode('', $section->getContent()))
);
$html = preg_replace_callback(
'/<img[^>]+>/',
array($this, 'replaceImg'),
$html
);
$html = preg_replace_callback(
'#<blockquote>.+</blockquote>#ms',
array($this, 'blubb'),
$html
);
$content[] = preg_replace_callback(
'/<a\s+(?P<attribs>[^>]*?\s+)?href="(?:(?!http:\/\/)[^#]*)#(?P<fragment>[^"]+)"/',
array($callback, 'render'),
$html
);
}
if ($renderNavigation) {
foreach ($this->docTree as $chapter) {
if ($chapter->getValue()->getId() === $section->getChapterId()) {
$navigation = array('<ul class="navigation">');
$this->docTree->prev();
$prev = $this->docTree->current();
if ($prev !== null) {
$prev = $prev->getValue();
$path = $zendUrlHelper->url(
array_merge(
$this->urlParams,
array(
'chapterId' => $this->encodeUrlParam($prev->getChapterId())
)
),
$this->url,
false,
false
);
$url = $view->url($path);
$url->setAnchor($this->encodeAnchor($prev->getId()));
$navigation[] = sprintf(
'<li class="prev"><a %shref="%s">%s</a></li>',
$prev->isNoFollow() ? 'rel="nofollow" ' : '',
$url->getAbsoluteUrl(),
$view->escape($prev->getTitle())
);
$this->docTree->next();
$this->docTree->next();
} else {
$this->docTree->rewind();
$this->docTree->next();
}
$url = $view->url($this->tocUrl);
$navigation[] = sprintf(
'<li><a href="%s">%s</a></li>',
$url->getAbsoluteUrl(),
mt('doc', 'Index')
);
$next = $this->docTree->current();
if ($next !== null) {
$next = $next->getValue();
$path = $zendUrlHelper->url(
array_merge(
$this->urlParams,
array(
'chapterId' => $this->encodeUrlParam($next->getChapterId())
)
),
$this->url,
false,
false
);
$url = $view->url($path);
$url->setAnchor($this->encodeAnchor($next->getId()));
$navigation[] = sprintf(
'<li class="next"><a %shref="%s">%s</a></li>',
$next->isNoFollow() ? 'rel="nofollow" ' : '',
$url->getAbsoluteUrl(),
$view->escape($next->getTitle())
);
}
$navigation[] = '</ul>';
$content = array_merge($navigation, $content, $navigation);
break;
}
}
}
return implode("\n", $content);
}
}

View File

@ -1,108 +0,0 @@
<?php
/* Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
namespace Icinga\Module\Doc;
use RecursiveIteratorIterator;
use Zend_View_Helper_Url;
use Icinga\Web\View;
/**
* TOC renderer
*/
class TocRenderer extends Renderer
{
/**
* The URL to replace links with
*
* @var string
*/
protected $url;
/**
* Additional URL parameters
*
* @var array
*/
protected $urlParams;
/**
* Content
*
* @var array
*/
protected $content = array();
/**
* Create a new toc renderer
*
* @param DocTree $docTree The documentation tree
* @param string $url The URL to replace links with
* @param array $urlParams Additional URL parameters
*/
public function __construct(DocTree $docTree, $url, array $urlParams)
{
parent::__construct($docTree, RecursiveIteratorIterator::SELF_FIRST);
$this->url = $url;
$this->urlParams = array_map(array($this, 'encodeUrlParam'), $urlParams);
}
public function beginIteration()
{
$this->content[] = '<nav><ul>';
}
public function endIteration()
{
$this->content[] = '</ul></nav>';
}
public function beginChildren()
{
$this->content[] = '<ul>';
}
public function endChildren()
{
$this->content[] = '</ul></li>';
}
/**
* Render the toc
*
* @param View $view
* @param Zend_View_Helper_Url $zendUrlHelper
*
* @return string
*/
public function render(View $view, Zend_View_Helper_Url $zendUrlHelper)
{
foreach ($this as $node) {
$section = $node->getValue();
/* @var $section \Icinga\Module\Doc\Section */
$path = $zendUrlHelper->url(
array_merge(
$this->urlParams,
array(
'chapterId' => $this->encodeUrlParam($section->getChapterId())
)
),
$this->url,
false,
false
);
$url = $view->url($path);
$url->setAnchor($this->encodeAnchor($section->getId()));
$this->content[] = sprintf(
'<li><a %shref="%s">%s</a>',
$section->isNoFollow() ? 'rel="nofollow" ' : '',
$url->getAbsoluteUrl(),
$view->escape($section->getTitle())
);
if (! $this->getInnerIterator()->current()->hasChildren()) {
$this->content[] = '</li>';
}
}
return implode("\n", $this->content);
}
}

View File

@ -1,9 +1,5 @@
/*! Icinga Web 2 | (c) 2013-2015 Icinga Development Team | GPLv2+ */
div.chapter {
padding-left: 5px;
}
.uppercase {
text-transform: uppercase;
}
@ -22,18 +18,6 @@ div.chapter {
background: linear-gradient(to bottom, @a 0%, @b 100%);
}
.box-shadow(@x: 3px; @y: 3px; @blur: 2px; @spread: 0px; @color: rgba(45, 45, 45, 0.75)) {
-webkit-box-shadow: @arguments;
-moz-box-shadow: @arguments;
box-shadow: @arguments;
}
.round-corners {
-moz-border-radius: 0.2em;
-webkit-border-radius: 0.2em;
border-radius: 0.2em;
}
table {
// Reset
border-collapse: collapse;
@ -77,7 +61,6 @@ thead {
.uppercase;
.bold;
}
position: sticky;
border-bottom: 0.25rem solid @icinga;
}
@ -105,31 +88,12 @@ pre > code {
.box-shadow;
}
div.chapter > ul.navigation {
ul.toc {
margin: 0;
padding: 0.4em;
text-align: center;
background-color: #888;
li {
list-style: none;
display: inline;
margin: 0.2em;
padding: 0;
a {
color: #fff;
text-decoration: none;
}
&.prev {
padding-right: 0.6em;
border-right: 2px solid #fff;
}
&.next {
padding-left: 0.6em;
border-left: 2px solid #fff;
}
}
padding: 0 0 0 1em;
}
.search-highlight {
color: #FBE012;
background: @icinga;
}

View File

@ -9,7 +9,7 @@ if (Icinga::app()->isCli()) {
}
$docModuleChapter = new Zend_Controller_Router_Route(
'doc/module/:moduleName/chapter/:chapterId',
'doc/module/:moduleName/chapter/:chapter',
array(
'controller' => 'module',
'action' => 'chapter',
@ -18,7 +18,7 @@ $docModuleChapter = new Zend_Controller_Router_Route(
);
$docIcingaWebChapter = new Zend_Controller_Router_Route(
'doc/icingaweb/chapter/:chapterId',
'doc/icingaweb/chapter/:chapter',
array(
'controller' => 'icingaweb',
'action' => 'chapter',

File diff suppressed because it is too large Load Diff