// der php hacker

// archiv

Workshop: Brawler – Not unplugged, Teil 3

Geschrieben am 08. Dez 2009 von Cem Derin

Zuvor:

  1. Workshop: Brawler – The Web Application Security Scanner, Teil 0
  2. Workshop: Brawler – Eine Frage der Lizenz, Teil 1
  3. Workshop: Brawler – Alles im Rahmen, Teil 2

Nachdem ich eine simple Grundarchitektur aufgebaut habe, möchte ich, dass die Software bereits in diesem Stadium über eine Plugin-Struktur erweiterbar wird. Also ermittle ich ein paar Anwendungsfälle, die das Plugin-System abdecken muss.

Ein- und Ausgabe manipulieren

Der einfachste Anwendungsfall wird das manipulieren von Ein- und Ausgabe in das Programm durch den Benutzer sein. Ein Anwendungsfall könnte sein: Die Daten müssen maschinenlesbar ausgegeben werden, damit externe Programme sie verarbeiten können. Ein anderer könnte ein silent-Mode sein, der Ausgaben lediglich in ein Logfile schreibt. Da sieht das Format ja auch schon etwas anders aus.

Actions von bestehenden Controllern manipulieren bzw ergänzen

Bevor ich komplett eigene Controller für mein Plugin schreibe, möchte ich versuchen bestehenden Code weiter zu verwenden und meine Ergänzungen zu integrieren. Konkreter Anwendungsfall wären zum Beispiel bestimmte Filterplugins, die bestimmte URLs ignorieren und ähnliches.

Überlegt man etwas genauer, so sollte sich das eigentlich nicht auf Controller beschränken, sondern wir wollen …

Methoden von Klassen ergänzen

Genau. Alle Klassen sollten dahingehend erweiterbar sein. Wirklich alle? Wahrscheinlich schon. An dieser Stelle halte ich aber mal im Hinterkopf, dass das ganze on Demand gelöst werden soll – und ich nicht jetzt schon alle Klassen dahingehend anpassen muss (aber kann ;) ).

Neue Controller hinzufügen

Zuguterletzt könnte es natürlich auch sein, dass ich ein ganz besonderes Plugin schreiben möchte, dass die Funktionalität von Brawler stark von seiner eigentlichen Bestimmung abweichen lässt. Hier wird es also evtl. nötig sein, neue Controller hinzufügen zu können.

Das Offensichtliche

Den Fall, dass ich Klassen hinzufügen können muss, habe ich mal weggelassen. Sind diese mitgeliefert, ist dies ohnehin kein Problem. Nicht ganz so offensichtlich, aber meiner Meinung nach immer noch auf der Hand liegend ist die Tatsache, dass Plugins auch Parameter entgegen nehmen können müssen – und dem System auch irgendwie mitteilen, dass diese gebraucht werden. Und genau das werde ich zu allererst implementieren.

Alles eine Frage der Argumente

Um die möglichen Argumente der Applikation zu ermitteln, habe ich die Methode getArguments in die Application Klasse implementiert:

		/**
		 * Returns an argument list
		 *
		 * @return Brawler_Plugin_Argument_List
		 */
		public static function getArguments() {
			// Application arguments
			// Argument List
			$list = new Brawler_Plugin_Argument_List();

			// define plugin directory
			$list->append(new Brawler_Plugin_Argument(
				'p',
				'Defines a plugin directory (default ./Plugins)',
				true
			));

			// append plugin directory
			$list->append(new Brawler_Plugin_Argument(
				'P',
				'Appends a plugin directory',
				true
			));

			// Plugin arguments
			// @TODO find a better way to merge ArrayObjects
			$plugins = Brawler_Plugin_Loader::getPlugins();
			$i = $plugins->getIterator();
			while($i->valid()) {
				$pluginArguments = $i->current()->getArguments();
				$k = $pluginArguments->getIterator();
				while($k->valid()) {
					$list->append($k->current());
					$k->next();
				}
				$i->next();
			}

			// Return list
			return $list;
		}

Zu allererst werden die Standardargumente der Applikation registriert. Danach holt sich die Applikation alle Plugins, liest deren Argumente aus und fügt sie der öffentlichen Liste hinzu die schließlich ausgegeben wird. Mir ist an dieser Stelle übrigens bewusst, dass das echt eine hässliche Methode ist, zwei ArrayObjects zu mergen.

Dem aufmerksamen Leser wird nicht entgangen sein, dass wir hier einen Plugin-Loader haben, der sich die Plugins holt. Schauen wir uns den doch mal etwas genauer an:

<?php

	/**
	 * Plugin Loader
	 *
	 * @package     Brawler
	 * @subpackage  Plugin
	 * @author      Cem Derin,
	 * @copyright   2009 Cem Derin,
	 */
	class Brawler_Plugin_Loader {
		/**
		 * Returns a plugin list
		 *
		 * @return Brawler_Plugin_List
		 */
		public static function getPlugins() {
			return self::_getPlugins();
		}

		/**
		 * Reads the plugin depending on configuration
		 *
		 * @return Brawler_Plugin_List
		 */
		protected static function _getPlugins() {
			if(!Brawler_Console::getArgument('p')) {
				$directory = realpath('src/Plugins/');
			} else {
				$directory = Brawler_Console::getArgument('p')->getValue();
			}

			if(Brawler_Console::getArgument('P')) {
				$directory.= PATH_SEPARATOR. Brawler_Console::getArgument('P')->getValue();
			}

			// Append Plugin Directorys to include Path
			set_include_path(get_include_path(). PATH_SEPARATOR. $directory);

			$dirs = split(PATH_SEPARATOR, $directory);

			$list = new Brawler_Plugin_List();
			foreach($dirs as $dir) {
				self::scanPlugins($dir, $list);
			}

			return $list;
		}

		/**
		 * Scans a path for plugins
		 *
		 * @param $dir String
		 * @param $list Brawler_Plugin_List
		 * @return Brawler_Plugin_List
		 */
		protected static function scanPlugins($dir, $list) {
			$dir =
				$dir. DIRECTORY_SEPARATOR.
				'Brawler'. DIRECTORY_SEPARATOR.
				'Plugin'. DIRECTORY_SEPARATOR;

			if(!file_exists($dir)) {
				return $list;
			}

			$files = scandir($dir);

			foreach($files as $file) {
				// excludes .svn stuff as well ;)
				if(substr($file, 0, 1) != '.') {
					if(is_dir($dir. DIRECTORY_SEPARATOR. $file)) {
						// determine classname
						$parts = array(
							'Brawler',
							'Plugin',
							$file,
							$file
						);

						$classname = implode('_', $parts);
						$list->append(new $classname);
					}
				}
			}

			return $list;
		}
	}
?>

Der Plugin-Loader greift als erstes Teilstück aktiv auf einen Parameter zu: p (oder P). Mit diesem kann der Benutzer später den Ort der Plugins bestimmen bzw. einen zusätzlichen hinzufügen. Diese Orte werden später in den include-Pfad aufgenommen. Das hat den einfachen Grund dass die Ordnerstruktur innerhalb des Plugin-Verzeichnisses genau der sonstigen entspricht bzw. entsprechen muss.

Um die Plugins zu ermitteln scannt der Loader alle Pluginverzeichnisse nach Plugins und lädt deren Hauptklasse. In der wiederum befindet sich derzeit lediglich eine Methode namens „getArguments“ die wiederum der aus der Applikation-Klasse sehr ähnelt (und auch sonst nichts neues mitbringt ;) ).

Damit eine Liste aller Argumente ausgegeben wird, habe ich den Help-Controller überarbeitet, dieser sieht nun so aus:

<?php
	/**
	 * Help Controller
	 *
	 * @package     Brawler
	 * @subpackage  Help
	 * @author      Cem Derin,
	 * @copyright   2009 Cem Derin,
	 */
	class Brawler_Controller_Help extends Brawler_Controller {
		/**
		 * Index action
		 * @return void
		 */
		public function indexAction() {
			$this->setView(new Brawler_View_Help_Index());
			$this->forward('help', 'showArguments');
		}

		/**
		 * Displays the argument list
		 *
		 * @return void
		 */
		public function showArgumentsAction() {
			$this->setView(new Brawler_View_Grid());

			// assemble rows
			$rows = new ArrayObject();
			$arguments = Brawler_Application::getArguments();
			$i = $arguments->getIterator();
			while($i->valid()) {
				$put = array();

				if($i->current()->hasValue()) {
					$put[] = '-'. $i->current()->getName().'=';
				} else {
					$put[] = '-'. $i->current()->getName();
				}

				$put[] = $i->current()->getDescription();

				$rows->append($put);

				$i->next();
			}

			$this->getView()->setRows($rows);
		}
	}
?>

Wir sehen, dass nach der Ausgabe der „usage“-Zeile direkt an die showArgumentsAction weitergereicht wird. Diese liest die Argumente aus den eben erwähnten Methoden aus, und macht aus jedem ein neues Array. Warum ist auch schnell ersichtlich: Es gibt eine neue View-Klasse: View_Grid. Mit dieser ist es möglich Tabellen darzustellen:

<?php

	/**
	 * View for grids
	 *
	 * @package     Brawler
	 * @author      Cem Derin,
	 * @copyright   2009 Cem Derin,
	 */
	class Brawler_View_Grid extends Brawler_View {
		/**
		 * Holds possible Labels
		 *
		 * @var ArrayObject
		 */
		protected $_labels = null;

		/**
		 * Appended rows
		 *
		 * @var ArrayObject
		 */
		protected $_rows = null;

		/**
		 * Holds the calculated column widths
		 *
		 * @var Array
		 */
		protected $_columnwidth = array();

		/**
		 * Holds the column padding
		 *
		 * @var Int
		 */
		protected $_padding = 2;

		/**
		 * Sets labels for the header row
		 *
		 * @param $labels ArrayObject
		 * @return void
		 */
		public function setLabels(ArrayObject $labels) {
			$this->_labels = $labels;
		}

		/**
		 * (non-PHPdoc)
		 * @see trunk/src/Brawler/Brawler_View#render()
		 */
		public function render() {
			if($this->_labels) {
				$this->_renderline($this->_labels);
			}

			if($this->_rows) {
				$i = $this->_rows->getIterator();
				while($i->valid()) {
					$this->_renderline($i->current());
					$i->next();
				}
			}
		}

		/**
		 * Sets the rows
		 * @param $rows ArrayObject
		 * @return void
		 */
		public function setRows(ArrayObject $rows) {
			$this->_rows = $rows;
		}

		/**
		 * Renders a single line
		 *
		 * @param $row
		 * @return void
		 */
		protected function _renderline($row) {
			if(!$this->_columnwidth) {
				$this->_determineColumnWidth();
			}

			$row = (array) $row;

			$i = 0;
			$print = '';
			foreach($row as $value) {
				$fillcount = $this->_columnwidth[$i] - strlen($value) + $this->_padding;
				$print.= $value. str_repeat(' ', $fillcount);
				$i++;
			}

			Brawler_Console_Output::output($print);
		}

		/**
		 * Determines the columns width
		 *
		 * @return void
		 */
		protected function _determineColumnWidth() {
			$i = $this->_rows->getIterator();
			while($i->valid()) {
				$r = (array) $i->current();

				$k = 0;
				foreach($r as $value) {
					if(!isset($this->_columnwidth[$k])) {
						// not set yet
						$this->_columnwidth[$k] = strlen($value);
					} else {
						// set only if greater
						if(strlen($value) > $this->_columnwidth[$k]) {
							$this->_columnwidth[$k] = strlen($value);
						}
					}

					$k++;
				}

				$i->next();
			}
		}
	}
?>

Die Properties sind schnell erklärt: _labels enthält evtl. eine evtl. gewünschte Titelzeile, _rows die entsprechenden folgenden Zeilen, _columnwidth enthält die berechneten Spaltenbreiten und _padding ermöglicht es, noch etwas Abstand zwischen die Spalten zu bekommen. Die einzig wirklich interessante Methode ist setRows, der direkt ein ArrayObject mit Objekten (oder weiteren Arrays) übergeben werden muss. Übrigens: Die einzelnen Zeilen müssen nicht zwangsläufig die gleiche Anzahl von Spalten besitzen ;)

Was kannst du?

Mit diesen Änderungen (die ich übrigens auch beim erscheinen der entsprechenden Beiträge hier schon ins SVN gestellt habe, das sich hier findet) sieht die Ausgabe also nun so aus:

Bild 2

?
Was kommt als nächstes

Der nächste Schritt wird ein Router sein, der den Programmfluss abhängig von Argumenten leitet. Außerdem werde ich ein paar Worte zu meinem Stil und meiner Arbeitsweise verlieren. Ich freue mich auf euer Feedback!

#001
09. Dez 2009

[...] Der PHP Hacker Zu Inhalt springen Über den PHP-HackerImpressum « Workshop: Brawler – Not unplugged, Teil 3 [...]

#002
11. Dez 2009

[...] Workshop: Brawler – Not unplugged, Teil 3 [...]

// kommentieren

// senden
theme von mir, software von wordpress, grid von 960 grid system. funktioniert in allen browsern, aber der safari bekommt das mit der schrift am schönsten hin.