From 871d28cd162e3f21579a2bddbb4d6f086417b85e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emmanuel=20Beno=C3=AEt?= Date: Sun, 5 Feb 2012 12:56:54 +0100 Subject: [PATCH] Importing bits and pieces This is the initial import based on a few files I had around. --- includes/box/package.inc.php | 6 + includes/box/view.inc.php | 124 +++++++++ includes/config-sample.inc.php | 7 + includes/core/controller.inc.php | 44 +++ includes/core/dao.inc.php | 20 ++ includes/core/database.inc.php | 133 +++++++++ includes/core/package.inc.php | 26 ++ includes/core/page.inc.php | 208 ++++++++++++++ includes/core/urls.inc.php | 103 +++++++ includes/core/view.inc.php | 117 ++++++++ includes/form/ctrl.inc.php | 73 +++++ includes/form/field.inc.php | 164 +++++++++++ includes/form/form.inc.php | 168 ++++++++++++ includes/form/modifiers.inc.php | 26 ++ includes/form/package.inc.php | 24 ++ includes/form/validators.inc.php | 99 +++++++ includes/form/view.inc.php | 184 +++++++++++++ includes/loader.inc.php | 456 +++++++++++++++++++++++++++++++ site/.htaccess | 5 + site/index.php | 7 + 20 files changed, 1994 insertions(+) create mode 100644 includes/box/package.inc.php create mode 100644 includes/box/view.inc.php create mode 100644 includes/config-sample.inc.php create mode 100644 includes/core/controller.inc.php create mode 100644 includes/core/dao.inc.php create mode 100644 includes/core/database.inc.php create mode 100644 includes/core/package.inc.php create mode 100644 includes/core/page.inc.php create mode 100644 includes/core/urls.inc.php create mode 100644 includes/core/view.inc.php create mode 100644 includes/form/ctrl.inc.php create mode 100644 includes/form/field.inc.php create mode 100644 includes/form/form.inc.php create mode 100644 includes/form/modifiers.inc.php create mode 100644 includes/form/package.inc.php create mode 100644 includes/form/validators.inc.php create mode 100644 includes/form/view.inc.php create mode 100644 includes/loader.inc.php create mode 100644 site/.htaccess create mode 100644 site/index.php diff --git a/includes/box/package.inc.php b/includes/box/package.inc.php new file mode 100644 index 0000000..5273d3c --- /dev/null +++ b/includes/box/package.inc.php @@ -0,0 +1,6 @@ +title = $title; + $this->URL = $URL; + } + + public function setID( $id ) + { + $this->id = $id; + return $this; + } + + public function setClass( $class ) + { + $this->class = $class; + return $this; + } + + public function setStyle( $style ) + { + $this->style = $style; + return $this; + } + + public function render( ) + { + return HTML::make( 'a' ) + ->setAttribute( 'title' , HTML::from( $this->title ) ) + ->setAttribute( 'href' , $this->URL ) + ->setAttribute( 'class' , + 'box-button' . ( ( $this->class === null ) + ? '' : ( ' ' . $this->class ) ) ) + ->setAttribute( 'style' , $this->style ) + ->setAttribute( 'id' , $this->id ) + ->appendElement( HTML::make( 'span' ) + ->appendText( $this->title ) ); + } + + + public static function create( $title , $URL ) + { + return new BoxButton( $title , $URL ); + } +} + + +final class View_Box + implements View +{ + protected $title; + protected $class; + protected $id; + + protected $buttons = array( ); + protected $contents; + + + public function __construct( $title , View $contents ) + { + $this->title = $title; + $this->contents = $contents; + } + + + public function setClass( $class ) + { + $this->class = $class; + return $this; + } + + + public function setID( $id ) + { + $this->id = $id; + return $this; + } + + + public function addButton( BoxButton $button ) + { + array_push( $this->buttons , $button ); + return $this; + } + + + public function render( ) + { + $box = HTML::make( 'div' ) + ->setAttribute( 'class' , 'box' . ( is_null( $this->class ) ? '' : " {$this->class}" ) ) + ->setAttribute( 'id' , $this->id ) + ->appendText( "\n" ); + + if ( ! is_null( $this->title ) ) { + $box->appendElement( HTML::make( 'h2' ) + ->setAttribute( 'class' , 'box-title' ) + ->appendText( $this->title ) ); + } + + if ( ! empty( $this->buttons )) { + $buttons = HTML::make( 'div' ) + ->setAttribute( 'class' , 'box-buttons' ); + foreach ( $this->buttons as $button ) { + $buttons->appendElement( $button->render( ) ); + } + $box->appendElement( $buttons ); + } + + $box->appendElement( HTML::make( 'div' ) + ->setAttribute( 'class' , 'box-contents' ) + ->append( $this->contents->render( ) ) ); + + return $box; + } +} diff --git a/includes/config-sample.inc.php b/includes/config-sample.inc.php new file mode 100644 index 0000000..2eff2a7 --- /dev/null +++ b/includes/config-sample.inc.php @@ -0,0 +1,7 @@ +getParameter( $name , 'POST' ); + } catch ( ParameterException $e ) { + return $this->getParameter( $name , 'GET' ); + } + } + + $from = '_' . $method; + global $$from; + if ( ! array_key_exists( $name , $$from ) ) { + throw new ParameterException( "$name/$method" ); + } + return ${$from}[ $name ]; + } + + public abstract function handle( Page $page ); +} + + +final class Ctrl_Simple + extends Controller +{ + private $view; + + public function __construct( View $view ) + { + $this->view = $view; + } + + public function handle( Page $page ) + { + return $this->view; + } +} diff --git a/includes/core/dao.inc.php b/includes/core/dao.inc.php new file mode 100644 index 0000000..11ff30f --- /dev/null +++ b/includes/core/dao.inc.php @@ -0,0 +1,20 @@ +database !== null ) { + throw new Exception( "trying to change DAO database" ); + } + $this->database = $database; + } + + protected final function query( $query , $prepare = false ) + { + return $this->database->query( $query , $prepare ); + } +} + diff --git a/includes/core/database.inc.php b/includes/core/database.inc.php new file mode 100644 index 0000000..8787467 --- /dev/null +++ b/includes/core/database.inc.php @@ -0,0 +1,133 @@ +package !== null ) { + throw new Exception( 'trying to call setPackage() twice' ); + } + $this->package = $package; + } + + + public function query( $query , $prepare = false ) + { + if ( ! $this->connection ) { + $this->connect( ); + } + if ( ! array_key_exists( $query , $this->queries ) + || ( $prepare && ! $this->queries[ $query ]->prepared( ) ) ) { + $this->queries[ $query ] = new DBQuery( $this->connection , $query , $prepare ); + } + return $this->queries[ $query ]; + } + + public function commit( ) + { + if ( ! $this->connection ) { + return; + } + if ( ! @pg_query( 'COMMIT' ) ) { + throw new DatabaseError( 'COMMIT: ' . pg_last_error( ) ); + } + exit( 0 ); + } + + public function connect( ) + { + $host = $this->package->config( 'db/host' , 'localhost' ); + $port = $this->package->config( 'db/port' ); + $name = $this->package->config( 'db/name' , null , true ); + $user = $this->package->config( 'db/user' ); + $pass = $this->package->config( 'db/password' ); + + $cString = array( ); + $cString[] = "host=$host"; + if ( $port !== null ) { + $cString[] = "port=$port"; + } + $cString[] = "dbname=$name"; + if ( $user !== null ) { + $cString[] = "user=$user"; + } + if ( $pass !== null ) { + $cString[] = "password=$pass"; + } + + $this->connection = pg_connect( join( ' ' , $cString ) ); + if ( ! $this->connection ) { + throw new DatabaseError( 'connection failed' ); + } + + if ( ! @pg_query( $this->connection , 'BEGIN TRANSACTION' ) ) { + throw new DatabaseError( 'BEGIN TRANSACTION: ' . pg_last_error( ) ); + } + } +} + + +final class DBQuery +{ + private static $lastStatementID = 0; + + private $connection; + private $query; + private $statement; + + + public function __construct( $connection , $query , $prepare = false ) + { + $this->connection = $connection; + $this->query = $query; + if ( $prepare ) { + $this->statement = 'prep_stmt_' . ( ++ DBQuery::$lastStatementID ); + if ( ! pg_prepare( $connection , $this->statement , $query ) ) { + throw new Exception( "unable to prepare statement '$query': " . pg_last_error( ) ); + } + } else { + $this->statement = null; + } + } + + public function __destruct( ) + { + if ( $this->statement !== null ) { + if ( ! pg_query( 'DEALLOCATE ' . $this->statement ) ) { + throw new Exception( "unable to deallocate statement: " . pg_last_error( ) ); + } + } + } + + + public function execute( ) + { + $arguments = func_get_args( ); + $result = array( ); + + if ( $this->statement !== null ) { + $pgResult = pg_execute( $this->connection , $this->statement , $arguments ); + } else { + $pgResult = pg_query_params( $this->connection , $this->query , $arguments ); + } + + if ( ! $pgResult ) { + throw new Exception( "query \"{$this->query}\" failed: " . pg_last_error( ) ); + } + + while ( $row = pg_fetch_object( $pgResult ) ) { + array_push( $result , $row ); + } + pg_free_result( $pgResult ); + + return $result; + } +} diff --git a/includes/core/package.inc.php b/includes/core/package.inc.php new file mode 100644 index 0000000..885a0d5 --- /dev/null +++ b/includes/core/package.inc.php @@ -0,0 +1,26 @@ +baseURL = dirname( $_SERVER[ 'SCRIPT_NAME' ] ); + } + + public final function addController( Controller $controller ) + { + array_push( $this->controllers , $controller ); + return $this; + } + + + public final function addView( View $view ) + { + array_push( $this->controllers , new Ctrl_Simple( $view ) ); + return $this; + } + + public final function getBaseURL( ) + { + return $this->baseURL; + } + + + protected abstract function render( ); + + protected function handleControllerValue( $rc ) + { + $rv = false; + if ( is_a( $rc , 'View' ) ) { + array_push( $this->views , $rc ); + } elseif ( is_a( $rc , 'Controller' ) ) { + $rv = $this->executeController( $rc ); + } elseif ( is_array( $rc ) ) { + foreach ( $rc as $rcItem ) { + if ( $this->handleControllerValue( $rcItem ) ) { + $rv = true; + break; + } + } + } elseif ( ! is_null( $rc ) ) { + header( "Location: $rc" ); + $rv = true; + } + return $rv; + } + + protected function executeController( Controller $controller ) + { + return $this->handleControllerValue( $controller->handle( $this ) ); + } + + public final function handle( ) + { + $mustDraw = true; + foreach ( $this->controllers as $controller ) { + if ( $this->executeController( $controller ) ) { + $mustDraw = false; + break; + } + } + if ( $mustDraw ) { + $this->render( ); + } + Loader::Singleton( 'Database' )->commit( ); + } +} + + +interface PathAware +{ + + public function setExtraPath( $path ); + +} + + +abstract class HTMLPage + extends Page + implements PackageAware +{ + protected $title; + protected $package; + + public function __construct( ) + { + parent::__construct( ); + } + + public function setPackage( Package $package ) + { + if ( $this->package !== null ) { + throw new Exception( 'trying to call setPackage() twice' ); + } + $this->package = $package; + } + + protected abstract function getMenu( ); + + + private function renderMenu( ) + { + $menu = $this->getMenu( ); + if ( empty( $menu ) ) { + return null; + } + + $html = HTML::make( 'ul' ) + ->setAttribute( 'class' , 'page-menu' ); + + foreach ( $menu as $link => $title ) { + $html->appendElement( HTML::make( 'li' ) + ->appendElement( HTML::make( 'a' ) + ->setAttribute( 'href' , $link ) + ->setAttribute( 'title' , HTML::from( $title ) ) + ->appendText( $title ) ) ); + } + + return $html; + } + + + public function setTitle( $title ) + { + $this->title = $title; + return $this; + } + + + protected function getHead( $title ) + { + return HTML::make( 'head' ) + ->appendElement( HTML::make( 'title' ) + ->appendText( $title ) ) + ->appendElement( HTML::make( 'meta' ) + ->setAttribute( 'http-equiv' , 'content-type' ) + ->setAttribute( 'content' , 'text/html;charset=UTF-8' ) ) + ->appendElement( HTML::make( 'style' ) + ->setAttribute( 'id' , 'main-style' ) + ->setAttribute( 'class' , 'css-style' ) + ->appendText( "\n@import url('" . $this->getBaseURL( ) + . "/style.css?1');\n" ) ); + } + + + protected function getBody( $title ) + { + $menu = $this->renderMenu( ); + $container = HTML::make( 'div' ) + ->setAttribute( 'class' , 'page-container' ); + + $t = HTML::make( 'h1' )->appendText( $title ); + if ( is_null( $menu ) ) { + $t->setAttribute( 'class' , 'no-menu' ); + } + $container->appendElement( $t ); + + if ( !is_null( $menu ) ) { + $container->append( $menu ); + } + + foreach ( $this->views as $view ) { + $container->append( $view->render( ) ); + } + + return HTML::make( 'body' )->appendElement( $container ); + } + + + public function render( ) + { + $baseTitle = $this->package->config( 'pages/baseTitle' , null , false ); + if ( $baseTitle === null ) { + $baseTitle = Loader::PackageConfig( 'core' )->get( 'pages/baseTitle' , '' , true ); + } + $title = is_null( $this->title ) ? '' : ( ' - ' . $this->title ); + $pTitle = is_null( $this->title ) ? $baseTitle : $this->title; + $title = $baseTitle . $title; + + header( 'Content-type: text/html; charset=utf-8' ); + echo HTML::make( 'html' ) + ->appendElement( $this->getHead( $title ) ) + ->appendElement( $this->getBody( $pTitle ) ) + ->getCode( ); + } +} + + +class Page_Basic + extends HTMLPage +{ + + protected function getMenu( ) + { + return array( ); + } + +} diff --git a/includes/core/urls.inc.php b/includes/core/urls.inc.php new file mode 100644 index 0000000..a992ce7 --- /dev/null +++ b/includes/core/urls.inc.php @@ -0,0 +1,103 @@ +prefix = $prefix; + $this->configBase = ( $prefix == null ) ? 'urls' : $prefix; + } + + public function setPackage( Package $package ) + { + if ( $this->package !== null ) { + throw new Exception( 'trying to call setPackage() twice' ); + } + $this->package = $package; + } + + + public function fromPathInfo( ) + { + if ( array_key_exists( 'PATH_INFO' , $_SERVER ) ) { + $path = $_SERVER[ 'PATH_INFO' ]; + } else { + $path = '/' . $this->package->config( $this->configBase . '/default' , 'home' ); + } + + $this->fromPath( $path ); + } + + public function fromPath( $path ) + { + if ( ! preg_match( '/^(\/[a-z0-9]+)+$/' , $path ) ) { + $this->showPageNotFound( ); + return; + } + + if ( $this->prefix == null ) { + $path = substr( $path , 1 ); + } else { + $path = $this->prefix . $path; + } + try { + $this->showPageFor( $path ); + } catch ( URLMapperException $e ) { + $this->showPageNotFound( $path ); + } + } + + + private function showPageNotFound( $requestPath ) + { + $path = $this->package->config( $this->configBase . '/errors/404', 'errors/404' ); + $this->showPageFor( $path . '/' . $requestPath , false ); + } + + + private function showPageFor( $path ) + { + $split = split( '/' , $path ); + $extras = array( ); + while ( !empty( $split ) ) { + $name = join( '_' , $split ); + try { + $page = Loader::Page( $name ); + break; + } catch ( LoaderException $e ) { + array_unshift( $extras , array_pop( $split ) ); + } + } + + if ( empty( $split ) ) { + throw new URLMapperException( $path ); + } + + $this->handlePage( $path , $page , join( '/' , $extras ) ); + } + + + private function handlePage( $requestPath , Page $page , $extraPath = '' , $pathFailure = true ) + { + if ( $page instanceof PathAware ) { + $success = $page->setExtraPath( $extraPath ); + } else { + $success = ( $extraPath === '' ); + } + + if ( $pathFailure && !$success ) { + $this->showPageNotFound( $requestPath ); + } else { + $page->handle( ); + } + } +} diff --git a/includes/core/view.inc.php b/includes/core/view.inc.php new file mode 100644 index 0000000..5ccd0e5 --- /dev/null +++ b/includes/core/view.inc.php @@ -0,0 +1,117 @@ +tag = $tag; + } + + + public function setAttribute( $attribute , $value ) + { + if ( $value !== null ) { + $this->attributes[ $attribute ] = $value; + $this->cached = null; + } + return $this; + } + + + public function appendText( $text ) + { + assert( is_scalar( $text ) ); + array_push( $this->contents , HTML::from( $text ) ); + $this->cached = null; + return $this; + } + + + public function appendElement( HTML $element ) + { + array_push( $this->contents , $element ); + $this->cached = null; + return $this; + } + + + public function appendRaw( $text ) + { + assert( is_scalar( $text ) ); + array_push( $this->contents , $text ); + $this->cached = null; + return $this; + } + + + public function append( $auto ) + { + if ( is_array( $auto ) ) { + foreach ( $auto as $element ) { + $this->append( $element ); + } + } elseif ( is_scalar( $auto ) ) { + $this->appendRaw( $auto ); + } else { + $this->appendElement( $auto ); + } + return $this; + } + + + public function getCode( ) + { + if ( $this->cached !== null ) { + return $this->cached; + } + $code = '<' . $this->tag; + + if ( ! empty( $this->attributes ) ) { + $attrs = array( ); + foreach ( $this->attributes as $name => $value ) { + array_push( $attrs , $name . '="' . $value . '"' ); + } + $code .= ' ' . join( ' ' , $attrs ); + } + + if ( empty( $this->contents ) ) { + $code .= ' />'; + } else { + $code .= '>'; + foreach ( $this->contents as $item ) { + if ( is_scalar( $item ) ) { + $code .= $item; + } else { + $code .= $item->getCode( ); + } + } + $code .= 'tag . '>'; + } + + return ( $this->cached = $code ); + } + + + public static function make( $tag ) + { + return new HTML( $tag ); + } + + + public static function from( $text ) + { + return htmlentities( $text , ENT_COMPAT , 'UTF-8' ); + } +} diff --git a/includes/form/ctrl.inc.php b/includes/form/ctrl.inc.php new file mode 100644 index 0000000..9c8a338 --- /dev/null +++ b/includes/form/ctrl.inc.php @@ -0,0 +1,73 @@ +form = $form; + } + + + protected function getValues( ) + { + $success = true; + + foreach ( $this->form->fields( ) as $field ) { + if ( $field === null ) { + continue; + } + + try { + $value = $this->getParameter( $field->name( ) , $this->form->method( ) ); + } catch ( ParameterException $e ) { + $value = null; + } + $field->setFormValue( $value ); + $vResult = $field->validate( ); + $success = $success && $vResult; + } + + return $success; + + } + + + protected function applyFormControllers( $page ) + { + foreach ( $this->form->controllers( ) as $controller ) { + $result = $controller->handle( $page ); + if ( $result === null ) { + continue; + } + return $result; + } + return null; + } + + + public function handle( Page $page ) + { + try { + $this->getParameter( $this->form->name( ) . '-submit' ); + } catch ( ParameterException $e ) { + return $this->form->view( ); + } + + if ( ! $this->getValues( ) ) { + return $this->form->view( ); + } + + $cResult = $this->applyFormControllers( $page ); + if ( $cResult === null ) { + return $this->form->view( ); + } + if ( $cResult ) { + return $this->form->successURL( ); + } + return $this->form->cancelURL( ); + } + +} diff --git a/includes/form/field.inc.php b/includes/form/field.inc.php new file mode 100644 index 0000000..289a915 --- /dev/null +++ b/includes/form/field.inc.php @@ -0,0 +1,164 @@ +type = $type; + $this->name = $name; + } + + + public function addOption( $value , $text , $disabled = false ) + { + assert( $this->type === 'select' ); + assert( ! array_key_exists( $value, $this->options ) ); + $obj = new stdClass( ); + $obj->text = $text; + $obj->disabled = $disabled; + $this->options[ $value ] = $obj; + return $this; + } + + public function options( ) + { + return $this->options; + } + + + public function name( ) + { + return $this->name; + } + + public function type( ) + { + return $this->type; + } + + + public function setDescription( $description ) + { + $this->description = $description; + return $this; + } + + public function description( ) + { + return $this->description; + } + + + public function setMandatory( $mandatory ) + { + $this->mandatory = $mandatory; + return $this; + } + + public function mandatory( ) + { + return $this->mandatory; + } + + + public function setModifier( FieldModifier $modifier ) + { + $this->modifier = $modifier; + return $this; + } + + + public function setValidator( FieldValidator $validator ) + { + $this->validator = $validator; + return $this; + } + + + public function setDefaultValue( $default ) + { + $this->valueDefault = $default; + return $this; + } + + public function setFormValue( $form ) + { + if ( $this->modifier !== null ) { + $form = $this->modifier->replace( $form ); + } + $this->valueForm = $form; + return $this; + } + + public function value( ) + { + return is_null( $this->valueForm ) + ? $this->valueDefault + : $this->valueForm; + } + + + public function putError( $error ) + { + $this->errors[ $error ] = 1; + return $this; + } + + public function errors( ) + { + return array_keys( $this->errors ); + } + + + public function validate( ) + { + $value = $this->value( ); + if ( $this->mandatory && ( $value === null || $value == '' ) ) { + $this->putError( Loader::Text( Field::MissingError ) ); + return false; + } + + if ( $this->validator !== null ) { + $errors = $this->validator->validate( $value ); + if ( is_array( $errors ) ) { + foreach ( $errors as $error ) { + $this->putError( $error ); + } + return false; + } + } + + return true; + } +} diff --git a/includes/form/form.inc.php b/includes/form/form.inc.php new file mode 100644 index 0000000..bb32429 --- /dev/null +++ b/includes/form/form.inc.php @@ -0,0 +1,168 @@ +buttonTitle = $buttonTitle; + $this->title = $title; + $this->name = is_null( $name ) ? 'the-form' : $name; + $this->action = '?'; + $this->method = 'POST'; + } + + + public function buttonTitle( ) + { + return $this->buttonTitle; + } + + public function name( ) + { + return $this->name; + } + + public function title( ) + { + return $this->title; + } + + public function action( ) + { + return $this->action; + } + + public function method( ) + { + return $this->method; + } + + public function cancelURL( ) + { + return $this->cancelURL; + } + + public function successURL( ) + { + return $this->successURL; + } + + public function fields( ) + { + return array_values( $this->fields ); + } + + public function field( $name ) + { + assert( array_key_exists( $name , $this->fields ) ); + return $this->fields[ $name ]; + } + + public function controllers( ) + { + return $this->controllers; + } + + + public function setAction( $action ) + { + $this->action = $action; + return $this; + } + + public function setMethod( $method ) + { + $this->method = $method; + return $this; + } + + public function setURL( $url ) + { + $this->cancelURL = $this->successURL = $url; + return $this; + } + + public function setCancelURL( $url ) + { + $this->cancelURL = $url; + return $this; + } + + public function setSuccessURL( $url ) + { + $this->successURL = $url; + return $this; + } + + + public function addField( Field $field ) + { + assert( ! array_key_exists( $field->name( ) , $this->fields ) ); + $this->fields[ $field->name( ) ] = $field; + return $this; + } + + + public function addSeparator( ) + { + $i = 0; + while ( array_key_exists( "sep$i" , $this->fields ) ) { + $i ++; + } + $this->fields[ "sep$i" ] = null; + return $this; + } + + + public function addController( Controller $controller ) + { + if ( is_a( $controller , 'FormAware' ) ) { + $controller->setForm( $this ); + } + array_push( $this->controllers , $controller ); + return $this; + } + + + public function controller( ) + { + return Loader::Ctrl( 'form' , $this ); + } + + + public function view( ) + { + $box = Loader::View( 'box' , $this->title , Loader::View( 'form' , $this ) ) + ->setClass( 'form' ); + + if ( $this->cancelURL !== null ) { + $box->addButton( BoxButton::create( Loader::Text( 'Cancel' ) , $this->cancelURL ) + ->setClass( 'form-cancel' ) ); + } + + return $box; + } +} + + +interface FormAware +{ + public function setForm( Form $form ); +} diff --git a/includes/form/modifiers.inc.php b/includes/form/modifiers.inc.php new file mode 100644 index 0000000..d54eb97 --- /dev/null +++ b/includes/form/modifiers.inc.php @@ -0,0 +1,26 @@ +removeDuplicateSpaces = $removeDuplicateSpaces; + } + + + public function replace( $value ) + { + if ( $value === null ) { + return ''; + } + $value = trim( $value ); + if ( $this->removeDuplicateSpaces ) { + $value = preg_replace( '/\s\s+/' , ' ' , $value ); + } + return $value; + } +} diff --git a/includes/form/package.inc.php b/includes/form/package.inc.php new file mode 100644 index 0000000..458cf18 --- /dev/null +++ b/includes/form/package.inc.php @@ -0,0 +1,24 @@ += $minLength ); + $this->errorPrefix = $errorPrefix; + $this->minLength = $minLength; + $this->maxLength = $maxLength; + } + + + public function validate( $value ) + { + $len = strlen( $value ); + if ( $len < $this->minLength ) { + $template = Loader::Text( '%1$s is too short (min. %2$d characters)' ); + return array( sprintf( $template , $this->errorPrefix , $this->minLength ) ); + } + if ( $this->maxLength !== null && $len > $this->maxLength ) { + $template = Loader::Text( '%1$s is too long (max. %2$d characters)' ); + return array( sprintf( $template , $this->errorPrefix , $this->maxLength ) ); + } + return null; + } +} + + +class Validator_InArray + implements FieldValidator +{ + private $values; + private $errorText; + + public function __construct( array $values , $errorText ) + { + $this->values = $values; + $this->errorText = $errorText; + } + + public function validate( $value ) + { + if ( ! ( empty( $this->values ) || in_array( $value , $this->values ) ) ) { + return array( $this->errorText ); + } + return null; + } +} + + +class Validator_IntValue + implements FieldValidator +{ + + private $invalidText; + private $minValue; + private $minError; + private $maxValue; + private $maxError; + + public function __construct( $invalidText ) + { + $this->invalidText = $invalidText; + } + + public function setMinValue( $minValue , $minError = null ) + { + assert( $this->maxValue === null || $minValue <= $this->maxValue ); + $this->minValue = $minValue; + $this->minError = ( $minError === null ) ? $this->invalidText : $minError; + return $this; + } + + public function setMaxValue( $maxValue , $maxError = null ) + { + assert( $this->minValue === null || $maxValue >= $this->maxValue ); + $this->maxValue = $maxValue; + $this->maxError = ( $maxError === null ) ? $this->invalidText : $maxError; + return $this; + } + + public function validate( $value ) + { + if ( !is_scalar( $value ) || (int) $value != $value ) { + return array( $this->invalidText ); + } else if ( $this->minValue !== null && $value < $this->minValue ) { + return array( $this->minError ); + } else if ( $this->maxValue !== null && $value > $this->maxValue ) { + return array( $this->maxError ); + } + } +} diff --git a/includes/form/view.inc.php b/includes/form/view.inc.php new file mode 100644 index 0000000..500f00c --- /dev/null +++ b/includes/form/view.inc.php @@ -0,0 +1,184 @@ +form = $form; + } + + + protected function renderHiddenFields( $prefix ) + { + $result = array( ); + foreach ( $this->form->fields( ) as $field ) { + if ( $field === null || $field->type( ) !== 'hidden' ) { + continue; + } + + array_push( $result , HTML::make( 'input' ) + ->setAttribute( 'type' , 'hidden' ) + ->setAttribute( 'name' , $field->name( ) ) + ->setAttribute( 'value' , HTML::from( $field->value( ) ) ) + ->setAttribute( 'id' , $prefix . $field->name( ) ) ); + } + return $result; + } + + + protected function renderPasswordField( $field , $prefix ) + { + return HTML::make( 'input' ) + ->setAttribute( 'type' , 'password' ) + ->setAttribute( 'name' , $field->name( ) ) + ->setAttribute( 'id' , $prefix . 'field' ) + ->setAttribute( 'class' , 'form-text-field' ); + } + + + protected function renderTextField( $field , $prefix ) + { + return HTML::make( 'input' ) + ->setAttribute( 'type' , 'text' ) + ->setAttribute( 'name' , $field->name( ) ) + ->setAttribute( 'id' , $prefix . 'field' ) + ->setAttribute( 'class' , 'form-text-field' ) + ->setAttribute( 'value' , HTML::from( $field->value( ) ) ); + } + + + protected function renderTextArea( $field , $prefix ) + { + return HTML::make( 'textarea' ) + ->setAttribute( 'name' , $field->name( ) ) + ->setAttribute( 'id' , $prefix . 'field' ) + ->setAttribute( 'class' , 'form-text-field' ) + ->appendText( (string) $field->value( ) ); + } + + + protected function renderSelectField( $field , $prefix ) + { + $select = HTML::make( 'select' ) + ->setAttribute( 'name' , $field->name( ) ) + ->setAttribute( 'id' , $prefix . 'field' ) + ->setAttribute( 'class' , 'form-select' ); + + $selected = $field->value( ); + foreach ( $field->options( ) as $value => $obj ) { + $option = HTML::make( 'option' ) + ->setAttribute( 'value' , $value ) + ->setAttribute( 'disabled' , $obj->disabled ? 'disabled' : null ) + ->appendText( $obj->text ); + if ( "$value" === "$selected" ) { + $option->setAttribute( 'selected' , 'selected' ); + } + $select->appendElement( $option ); + } + return $select; + } + + + protected function renderField( $field , $prefix ) + { + switch ( $field->type( ) ) { + + case 'password': + $result = $this->renderPasswordField( $field , $prefix ); + break; + + case 'text': + $result = $this->renderTextField( $field , $prefix ); + break; + + case 'textarea': + $result = $this->renderTextArea( $field , $prefix ); + break; + + case 'select': + $result = $this->renderSelectField( $field , $prefix ); + break; + + default: + throw new Exception( "field " . $field->name() . " has unknown type " . $field->type() ); + + } + return $result; + } + + + protected function renderVisibleFields( $target , $prefix ) + { + foreach ( $this->form->fields( ) as $field ) { + if ( $field === null ) { + $target->appendElement( HTML::make( 'hr' ) ); + continue; + } + if ( $field->type( ) === 'hidden' ) { + continue; + } + + $fPrefix = $prefix . $field->name( ) . '-'; + if ( $field->type( ) === 'html' ) { + $target->appendElement( HTML::make( 'dd' ) + ->setAttribute( 'id' , $fPrefix ) + ->setAttribute( 'class' , 'html-section' ) + ->append( $field->value( ) ) ); + continue; + } + + $fClass = 'field' . ( $field->mandatory( ) ? ' mandatory' : '' ); + $target->appendElement( HTML::make( 'dt' ) + ->setAttribute( 'class' , $fClass ) + ->setAttribute( 'id' , $fPrefix . 'label' ) + ->appendElement( HTML::make( 'label' ) + ->setAttribute( 'for' , $fPrefix . 'field' ) + ->appendText( $field->description( ) ) ) ); + + $errors = $field->errors( ); + if ( !empty( $errors ) ) { + foreach ( $errors as $error ) { + $target->appendElement( HTML::make( 'dd' ) + ->setAttribute( 'class' , 'form-error' ) + ->appendText( $error ) ); + } + $fClass .= ' erroneous'; + } + + $target->appendElement( HTML::make( 'dd' ) + ->setAttribute( 'id' , $fPrefix . 'container' ) + ->setAttribute( 'class' , $fClass ) + ->append( $this->renderField( $field , $fPrefix ) ) ); + } + } + + + public function render( ) + { + $name = $this->form->name(); + $prefix = $name . '-'; + + $form = HTML::make( 'form' ) + ->setAttribute( 'name' , $name ) + ->setAttribute( 'id' , $prefix . 'form' ) + ->setAttribute( 'action' , $this->form->action( ) ) + ->setAttribute( 'method' , $this->form->method( ) ) + ->append( $this->renderHiddenFields( $prefix ) ) + ->append( $visibleArea = HTML::make( 'dl' ) ); + + $this->renderVisibleFields( $visibleArea , $prefix ); + $visibleArea->appendElement( HTML::make( 'dt' ) + ->setAttribute( 'class' , 'submit-button' ) + ->setAttribute( 'id' , $prefix . 'submit-container' ) + ->appendElement( HTML::make( 'input' ) + ->setAttribute( 'type' , 'submit' ) + ->setAttribute( 'name' , $prefix . 'submit' ) + ->setAttribute( 'id' , $prefix . 'submit' ) + ->setAttribute( 'value' , $this->form->buttonTitle( ) ) ) ); + + return $form; + } +} diff --git a/includes/loader.inc.php b/includes/loader.inc.php new file mode 100644 index 0000000..ead5830 --- /dev/null +++ b/includes/loader.inc.php @@ -0,0 +1,456 @@ +package = $package; + $this->config = $config; + } + + + public function get( $path = '' , $default = null , $fail = false ) + { + if ( $path == '' ) { + return $this->config; + } + + $aPath = explode( '/' , $path ); + $config = &$this->config; + foreach ( $aPath as $name ) { + if ( !( is_array( $config ) && array_key_exists( $name , $config ) ) ) { + if ( $fail ) { + throw new LoaderException( "configuration key '$path' not found for package '" + . $this->package->name() . "'" ); + } + return $default; + } + $config = &$config[ $name ]; + } + return $config; + } +} + + +final class Package +{ + private $name; + + private $files; + private $requires; + + private $ctrls; + private $daos; + private $extras; + private $hooks; + private $pages; + private $singletons; + private $views; + + private $loaded = false; + private $config; + + public function __construct( $name , $description , $config ) + { + $this->name = $name; + + $fields = array( 'files' , 'requires' , 'daos' , 'views' , 'ctrls' , 'extras' , 'singletons' , 'pages' , 'hooks' ); + foreach ( $fields as $field ) { + $this->getField( $description , $field ); + } + if ( empty( $this->files ) ) { + throw new LoaderException( "package '{$this->name}': no files" ); + } + + if ( ! is_array( $config ) ) { + $config = array( ); + } + $this->config = new ConfigGetter( $this , $config ); + } + + + private function getField( $description , $field ) + { + if ( ! array_key_exists( $field , $description ) ) { + $this->$field = array( ); + return; + } + + $value = $description[ $field ]; + if ( !is_array( $value ) ) { + throw new LoaderException( "package '{$this->name}': '$field' must be an array" ); + } + + foreach ( $value as $item ) { + if ( !is_string( $item ) ) { + throw new LoaderException( "package '{$this->name}': '$field' contains non-string items" ); + } + } + + $this->$field = $value; + } + + + public function name( ) + { + return $this->name; + } + + public function files( ) + { + return $this->files; + } + + public function requires( ) + { + return $this->requires; + } + + public function daos( ) + { + return $this->daos; + } + + public function views( ) + { + return $this->views; + } + + public function ctrls( ) + { + return $this->ctrls; + } + + public function singletons( ) + { + return $this->singletons; + } + + public function extras( ) + { + return $this->extras; + } + + public function pages( ) + { + return $this->pages; + } + + + public function loaded( ) + { + return $this->loaded; + } + + public function hooks( ) + { + return $this->hooks; + } + + public function setLoaded( ) + { + $this->loaded = true; + } + + + public function config( $path = '' , $default = null , $fail = false ) + { + return $this->config->get( $path , $default , $fail ); + } + + public function getConfigAccess( ) + { + return $this->config; + } +} + + +interface PackageAware +{ + public function setPackage( Package $package ); +} + + +interface TextSource +{ + public function get( $what ); +} + + +final class Loader +{ + private static $loader = null; + + private $baseDir; + private $config; + private $packages = array( ); + private $items = array( + 'ctrls' => array( ) , + 'daos' => array( ) , + 'extras' => array( ) , + 'singletons' => array( ) , + 'views' => array( ) , + 'pages' => array( ) , + ); + private $loading = array( ); + private $singletons = array( ); + private $daos = array( ); + private $textSource; + + private function __construct( ) + { + $this->baseDir = dirname( __FILE__ ); + $this->loadConfig( ); + $this->loadPackageDescriptions( ); + } + + private function loadConfig( ) + { + $config = array( ); + @include( $this->baseDir . '/config.inc.php' ); + $this->config = $config; + } + + private function loadPackageDescriptions( ) + { + if ( !( $dh = opendir( $this->baseDir ) ) ) { + throw new LoaderException( "unable to access directory" ); + } + + while ( ( $entry = readdir( $dh ) ) !== false ) { + if ( $entry === '.' || $entry === '..' ) { + continue; + } + + $path = "{$this->baseDir}/$entry"; + if ( is_dir( $path ) && is_file( "$path/package.inc.php" ) ) { + $this->loadDescription( $entry ); + } + } + + closedir( $dh ); + } + + + private function loadDescription( $name ) + { + $package = array( ); + require( $this->baseDir . '/' . $name . '/package.inc.php' ); + if ( empty( $package ) ) { + throw new LoaderException( "package '$name': no information" ); + } + + if ( ! array_key_exists( $name , $this->config ) ) { + $this->config[ $name ] = array( ); + } + + $package = new Package( $name , $package , $this->config[ $name ] ); + $this->packages[ $name ] = $package; + $this->config[ $name ] = null; + + foreach ( array_keys( $this->items ) as $type ) { + $items = $package->$type( ); + foreach ( $items as $item ) { + if ( array_key_exists( $item , $this->items[ $type ] ) ) { + $oName = $this->items[ $type ][ $item ]; + $type = substr( $type , 0 , strlen( $type ) - 1 ); + throw new LoaderException( "package '$name': conflict with '$oName' on $type '$item'" ); + } + $this->items[ $type ][ $item ] = $name; + } + } + } + + + private function loadPackage( $name ) + { + if ( ! array_key_exists( $name , $this->packages ) ) { + throw new LoaderException( "Package '$name' not found" ); + } + + $package = $this->packages[ $name ]; + if ( $package->loaded( ) ) { + return; + } + + if ( array_key_exists( $name , $this->loading ) ) { + throw new LoaderException( "Package '$name': recursive dependencies detected" ); + } + $this->loading[ $name ] = 1; + + foreach ( $package->requires( ) as $dependency ) { + $this->loadPackage( $dependency ); + } + + $dir = $this->baseDir . '/' . $name; + foreach ( $package->files( ) as $file ) { + require_once( "$dir/$file.inc.php" ); + } + + unset( $this->loading[ $name ] ); + $package->setLoaded( ); + + $hooks = $package->hooks( ); + if ( is_array( $hooks ) ) { + foreach ( $hooks as $hook ) { + $hook( $this , $package ); + } + } + } + + + private function findItem( $name , $type ) + { + $rType = $type . 's'; + if ( ! array_key_exists( $rType , $this->items ) ) { + throw new LoaderException( "Invalid type '$type'" ); + } + + if ( ! array_key_exists( $name , $this->items[ $rType ] ) ) { + throw new LoaderException( "Item '$name' of type $type not found" ); + } + + $package = $this->items[ $rType ][ $name ]; + $this->loadPackage( $package ); + return $this->packages[ $package ]; + } + + + private function createInstance( $package , $cName , $args ) + { + if ( empty( $args ) ) { + $instance = new $cName(); + } else { + $reflection = new ReflectionClass( $cName ); + $instance = $reflection->newInstanceArgs( $args ); + } + + if ( is_a( $instance , 'PackageAware' ) ) { + $instance->setPackage( $package ); + } + return $instance; + } + + + private function loadAndCreate( $type , $name , $cName , $args ) + { + $package = $this->findItem( $name , $type ); + return $this->createInstance( $package , $cName , $args ); + } + + + private function getSingleton( $name ) + { + if ( ! array_key_exists( $name , $this->singletons ) ) { + $this->singletons[ $name ] = $this->loadAndCreate( 'singleton' , $name , $name , array( ) ); + } + return $this->singletons[ $name ]; + } + + + private function getDao( $name ) + { + if ( array_key_exists( $name , $this->daos ) ) { + return $this->daos[ $name ]; + } + $cName = Loader::convertName( 'DAO' , $name ); + $instance = $this->loadAndCreate( 'dao' , $name , $cName , array( ) ); + $instance->setDatabase( Loader::Singleton( 'Database' ) ); + $this->daos[ $name ] = $instance; + return $instance; + } + + + private static function get( ) + { + if ( Loader::$loader === null ) { + Loader::$loader = new Loader( ); + } + return Loader::$loader; + } + + private static function convertName( $type , $name ) + { + $cName = ucfirst( $type ) . '_'; + foreach ( explode( '_' , $name ) as $part ) { + $cName .= ucfirst( $part ); + } + return $cName; + } + + private static function creator( $type , $convert , $args ) + { + $name = array_shift( $args ); + $cName = $convert ? Loader::convertName( $type , $name ) : $name; + return Loader::get( )->loadAndCreate( $type , $name , $cName , $args ); + } + + + public static function PackageConfig( $name ) + { + $loader = Loader::get( ); + $loader->loadPackage( $name ); + return $loader->packages[ $name ]->getConfigAccess( ); + } + + + public static function TextSource( TextSource $source = null ) + { + if ( $source !== null ) { + Loader::get( )->textSource = $source; + } else { + return Loader::get( )->textSource; + } + } + + + public static function Text( $what ) + { + $source = Loader::get( )->textSource; + return $source ? $source->get( $what ) : $what; + } + + + public static function Load( $name , $type = 'extra' ) + { + Loader::get( )->findItem( $name , $type ); + } + + + public static function Singleton( $name ) + { + return Loader::get( )->getSingleton( $name ); + } + + public static function Create( ) + { + return Loader::creator( 'extra' , false , func_get_args( ) ); + } + + public static function View( ) + { + return Loader::creator( 'view' , true , func_get_args( ) ); + } + + public static function Ctrl( ) + { + return Loader::creator( 'ctrl' , true , func_get_args( ) ); + } + + public static function Page( ) + { + return Loader::creator( 'page' , true , func_get_args( ) ); + } + + public static function DAO( $name ) + { + return Loader::get( )->getDao( $name ); + } +} diff --git a/site/.htaccess b/site/.htaccess new file mode 100644 index 0000000..bb5709d --- /dev/null +++ b/site/.htaccess @@ -0,0 +1,5 @@ +RewriteEngine on +#RewriteBase / +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d +RewriteRule ^(.*)$ index.php/$1 [L,QSA] diff --git a/site/index.php b/site/index.php new file mode 100644 index 0000000..b16abf9 --- /dev/null +++ b/site/index.php @@ -0,0 +1,7 @@ +fromPathInfo( ); + + +?>