├── .gitignore ├── src ├── ufront │ ├── ufadmin │ │ ├── view │ │ │ ├── welcome.html │ │ │ ├── layout.html │ │ │ ├── container.html │ │ │ └── login.html │ │ ├── UFAdminPermissions.hx │ │ ├── modules │ │ │ └── DBAdminModule.hx │ │ ├── UFAdminModule.hx │ │ └── controller │ │ │ └── UFAdminHomeController.hx │ └── spadm │ │ └── DBAdmin.hx └── spadm │ ├── Custom.hx │ ├── Id.hx │ ├── AdminStyle.hx │ ├── Serialized.hx │ ├── TableInfos.hx │ └── Admin.hx ├── README.md ├── bundle.sh ├── haxelib.json └── LICENSE.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.zip 2 | -------------------------------------------------------------------------------- /src/ufront/ufadmin/view/welcome.html: -------------------------------------------------------------------------------- 1 |
2 |

Hi Boss!

3 |

This is the ufront admin interface.

4 |
-------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ufront-ufadmin 2 | ============== 3 | 4 | The automatic admin website for your ufront project. Administer admin-tasks, migrations, backend data and auth details. 5 | -------------------------------------------------------------------------------- /bundle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | libname='ufront-ufadmin' 4 | rm -f "${libname}.zip" 5 | zip -r "${libname}.zip" haxelib.json src LICENSE.txt README.md 6 | echo "Saved as ${libname}.zip" 7 | -------------------------------------------------------------------------------- /src/ufront/ufadmin/UFAdminPermissions.hx: -------------------------------------------------------------------------------- 1 | package ufront.ufadmin; 2 | 3 | enum UFAdminPermissions 4 | { 5 | UFACanRunMigrations; 6 | UFACanRunAdminTasks; 7 | UFACanAccessAdminArea; 8 | } -------------------------------------------------------------------------------- /src/ufront/ufadmin/view/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ::title:: 5 | 6 | 7 | 8 | 9 |
10 | ::viewContent:: 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/ufront/ufadmin/view/container.html: -------------------------------------------------------------------------------- 1 |

Ufront Admin Console

2 |
3 |
4 | 11 |
12 |
13 | 14 |
15 |
-------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ufront-ufadmin", 3 | "license": "MIT", 4 | "classPath": "src", 5 | "tags": ["ufront","web","neko","dashboard","admin"], 6 | "description": "A simple web-based admin console for your ufront website. Has support for Nicolas' DBAdmin tool, and very simple integrations with ufront-easyauth, ufront-mail etc. Is pluggable so you can create your own admin modules.", 7 | "contributors": ["fponticelli","jason","andyli","kevinresol"], 8 | "releasenote": "Remove explicit hscript version.", 9 | "version": "1.0.0-beta.14", 10 | "url": "https://github.com/ufront/ufront-admin", 11 | "dependencies": { 12 | "ufront-mvc": "", 13 | "ufront-orm": "", 14 | "ufront-easyauth": "", 15 | "hscript": "" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ufront/ufadmin/view/login.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

UF Admin Login

5 |

To access the Ufront Admin area you must be logged in.

6 |
7 |
8 |
9 |
10 | 11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 | 20 |
21 | 22 | 23 |
24 |
-------------------------------------------------------------------------------- /src/ufront/ufadmin/modules/DBAdminModule.hx: -------------------------------------------------------------------------------- 1 | package ufront.ufadmin.modules; 2 | 3 | import ufront.ufadmin.UFAdminModule; 4 | 5 | class DBAdminModule extends UFAdminModule { 6 | public function new() { 7 | super( "db", "Database Admin" ); 8 | } 9 | 10 | override public function checkPermissions() { 11 | // Currently anyone who has access to ufadmin has access to this DBAdminModule. 12 | return true; 13 | } 14 | 15 | @:route("/*") 16 | public function doDefault() { 17 | #if server 18 | if ( sys.db.Manager.cnx==null ) { 19 | return 'No Database Connection Found'; 20 | } 21 | else { 22 | var base = context.generateUri( baseUri.substr(1) ); 23 | spadm.AdminStyle.BASE_URL = base; 24 | ufront.spadm.DBAdmin.handler(base); 25 | context.completion.set( CRequestHandlersComplete ); 26 | context.completion.set( CFlushComplete ); 27 | return null; 28 | } 29 | #elseif 30 | return 'DBAdminModule is not available on the client'; 31 | #end 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Jason O'Neil 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /src/spadm/Custom.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c)2012 Nicolas Cannasse 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining 5 | * a copy of this software and associated documentation files (the 6 | * "Software"), to deal in the Software without restriction, including 7 | * without limitation the rights to use, copy, modify, merge, publish, 8 | * distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so, subject to 10 | * the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be 13 | * included in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | package spadm; 24 | 25 | typedef SearchInfos = { 26 | var fields : Array; 27 | var names : Array; 28 | var values : Array; 29 | } 30 | 31 | typedef RightsInfos = { 32 | var readOnly : Array; 33 | var invisible : Array; 34 | var can : { 35 | var insert : Bool; 36 | var modify : Bool; 37 | var delete : Bool; 38 | var truncate : Bool; 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /src/spadm/Id.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c)2012 Nicolas Cannasse 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining 5 | * a copy of this software and associated documentation files (the 6 | * "Software"), to deal in the Software without restriction, including 7 | * without limitation the rights to use, copy, modify, merge, publish, 8 | * distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so, subject to 10 | * the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be 13 | * included in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | package spadm; 24 | 25 | class Id { 26 | 27 | public static function encode( id : String ) : Int { 28 | var l = id.length; 29 | if( l > 6 ) 30 | throw "Invalid identifier '"+id+"'"; 31 | var k = 0; 32 | var p = l; 33 | while( p > 0 ) { 34 | var c = id.charCodeAt(--p) - 96; 35 | if( c < 1 || c > 26 ) { 36 | c = c + 96 - 48; 37 | if( c >= 1 && c <= 5 ) 38 | c += 26; 39 | else 40 | throw "Invalid character "+id.charCodeAt(p)+" in "+id; 41 | } 42 | k <<= 5; 43 | k += c; 44 | } 45 | return k; 46 | } 47 | 48 | public static function decode( id : Int ) : String { 49 | var s = new StringBuf(); 50 | if( id < 1 ) { 51 | if( id == 0 ) return ""; 52 | throw "Invalid ID "+id; 53 | } 54 | while( id > 0 ) { 55 | var k = id & 31; 56 | if( k < 27 ) 57 | s.addChar(k + 96); 58 | else 59 | s.addChar(k + 22); 60 | id >>= 5; 61 | } 62 | return s.toString(); 63 | } 64 | 65 | } -------------------------------------------------------------------------------- /src/ufront/ufadmin/UFAdminModule.hx: -------------------------------------------------------------------------------- 1 | package ufront.ufadmin; 2 | 3 | import ufront.web.Controller; 4 | import ufront.web.result.ViewResult; 5 | import ufront.view.TemplateData; 6 | 7 | /** 8 | A base-class used to define a controller that can be used in the UFAdmin control panel. 9 | 10 | It adds "slug", "title" and "checkPermissions" so that menus appropriate for the current user can be constructed. 11 | **/ 12 | class UFAdminModule extends Controller { 13 | 14 | /** The url slug for this controller. Should be URL-friendly (a-zA-Z0-9_-) **/ 15 | public var slug:String; 16 | 17 | /** The title of this module, to show in side menus etc **/ 18 | public var title:String; 19 | 20 | function new( slug:String, title:String ) { 21 | super(); 22 | this.slug = slug; 23 | this.title = title; 24 | } 25 | 26 | @:route( "this-is-a-workaround" ) 27 | function thisIsAWorkaround():Void { 28 | // Currently there is no way to skip checking for @:route metadata in the build macro. 29 | // Until I set that up, this route / method exists solely to silence the error message. 30 | // Don't hate me! 31 | } 32 | 33 | /** 34 | Check if the current auth session has permission to use this module. 35 | 36 | Use `context.auth.checkPermission()` etc to check. 37 | 38 | If this does not return true, the module will not be added to menu, and the user will not be able to access this module. 39 | 40 | By default this simply returns true. 41 | **/ 42 | public function checkPermissions():Bool { 43 | return true; 44 | } 45 | 46 | /** 47 | A helper function for UFAdmin modules to wrap their content in a layout. 48 | The layout has a simple HTML structure, the bootstrap stylesheet, and a base HREF relative the `/ufadmin/` path. 49 | 50 | @param title The title of the current page. 51 | @param template The string of the template to use. We recommend including this using `CompileTime.readFile(view.html)` or similar to avoid runtime dependencies. 52 | @param data The `TemplateData` to use when rendering the template. 53 | @return ViewResult A ready to use ViewResult. 54 | **/ 55 | public static function wrapInLayout( title:String, template:String, data:TemplateData ):ViewResult { 56 | return new ViewResult( data ) 57 | .setVar( "title", title ) 58 | .usingTemplateString( 59 | template, 60 | CompileTime.readFile( "ufront/ufadmin/view/layout.html" ) 61 | ); 62 | } 63 | } -------------------------------------------------------------------------------- /src/ufront/spadm/DBAdmin.hx: -------------------------------------------------------------------------------- 1 | package ufront.spadm; 2 | 3 | #if server 4 | import sys.db.Manager; 5 | import spadm.TableInfos; 6 | import ufront.db.Object; 7 | import ufront.db.Relationship; 8 | using StringTools; 9 | using Lambda; 10 | 11 | /** 12 | * An extension of DBAdmin that co-operates better with ufront.db classes 13 | * 14 | * Changes 15 | * - Ignore classes with @noTable metadata 16 | * - Find classes using CompileTime.getAllClasses() instead 17 | * - Check relationships and add ManyToMany tables 18 | * 19 | * It has a handler() static method that is used in the exact same way as spadm.Admin.handler 20 | * for processing the actual requests. 21 | */ 22 | class DBAdmin extends spadm.Admin 23 | { 24 | public var manyToManyTableNames:Array; 25 | 26 | public function new() 27 | { 28 | super(); 29 | manyToManyTableNames = []; 30 | } 31 | 32 | override function getTables() 33 | { 34 | var tables:Array = new Array(); 35 | 36 | var classes = CompileTime.getAllClasses(sys.db.Object); 37 | for (cl in classes) 38 | { 39 | addTable(tables, cl); 40 | } 41 | 42 | tables.sort(function(t1,t2) { return if( t1.name.toUpperCase() > t2.name.toUpperCase() ) 1 else if( t1.name.toUpperCase() < t2.name.toUpperCase() ) -1 else 0; }); 43 | return tables; 44 | } 45 | 46 | @:access(spadm.TableInfos) 47 | function addTable(tables:Array, model:Class) 48 | { 49 | // If no RTTI, don't add 50 | if( haxe.rtti.Meta.getType(model).rtti == null ) return; 51 | 52 | // If @noTable metadata, don't add 53 | var m = haxe.rtti.Meta.getType(model); 54 | if ( Reflect.hasField(m, "noTable") ) return; 55 | 56 | // Search relations for ManyToMany tables 57 | var rels:Array = Reflect.field(model, "hxRelationships"); 58 | 59 | // Looking for osmething like: students,ManyToMany,app.coredata.model.Student 60 | // for (r in rels) 61 | // { 62 | // var parts = r.split(","); 63 | // switch (parts) 64 | // { 65 | // case [_,"ManyToMany",bClassName]: 66 | // var aClassName = Type.getClassName(model); 67 | // var tableName = ManyToMany.generateTableName(model, Type.resolveClass(bClassName)); 68 | 69 | // // If we haven't already created this join table 70 | // if (manyToManyTableNames.has(tableName) == false) 71 | // { 72 | // // Keep track of it 73 | // manyToManyTableNames.push(tableName); 74 | 75 | // // Sys.println('$field:ManyToMany<$aClassName,$bClassName> = $tableName'.htmlEscape()+"
"); 76 | 77 | // // Create a TableInfos for Relationship, then change the table name - see how it goes... 78 | // var ti = new TableInfos( Type.getClassName(Relationship) ); 79 | // // public var cl(default,null) : Class; 80 | // // public var name(default,null) : String; 81 | // // public var className(default,null) : String; 82 | // // public var manager : Manager; 83 | // Sys.println('${ti.name} ${ti.className}
'); 84 | // ti.name = tableName; 85 | // tables.push(ti); 86 | // } 87 | // default: 88 | // // do nothing 89 | // } 90 | // } 91 | 92 | // Look for ManyToMany relations 93 | tables.push(new TableInfos(Type.getClassName(model))); 94 | } 95 | 96 | public static function handler( ?baseUrl:String ) 97 | { 98 | Manager.initialize(); // make sure it's been done 99 | try { 100 | new DBAdmin().process(baseUrl); 101 | } catch( e : Dynamic ) { 102 | // rollback in case of multiple delete/update - no effect on DB struct changes 103 | // since they are done outside of transaction 104 | Sys.print("
");
105 | 				Sys.println(Std.string(e));
106 | 				Sys.println(haxe.CallStack.toString(haxe.CallStack.exceptionStack()));
107 | 				Sys.println(haxe.CallStack.toString(haxe.CallStack.exceptionStack()));
108 | 				Sys.print("
"); 109 | Manager.cnx.rollback(); 110 | } 111 | } 112 | } 113 | #end 114 | -------------------------------------------------------------------------------- /src/ufront/ufadmin/controller/UFAdminHomeController.hx: -------------------------------------------------------------------------------- 1 | package ufront.ufadmin.controller; 2 | 3 | 4 | import ufront.web.context.HttpContext; 5 | import ufront.web.Controller; 6 | import ufront.web.result.ActionResult; 7 | import ufront.web.result.*; 8 | import ufront.auth.model.*; 9 | import ufront.auth.*; 10 | import ufront.auth.AuthError; 11 | import ufront.web.HttpError; 12 | import ufront.view.TemplateData; 13 | import haxe.ds.StringMap; 14 | import ufront.auth.api.EasyAuthApi; 15 | import tink.CoreApi; 16 | import minject.Injector; 17 | using haxe.io.Path; 18 | using Lambda; 19 | using StringTools; 20 | 21 | #if server 22 | /** 23 | A simple admin area for your site. 24 | 25 | Default modules include: 26 | 27 | - DBAdminModule 28 | 29 | More can be added using `addModule()` 30 | **/ 31 | class UFAdminHomeController extends Controller { 32 | // 33 | // Member variables / methods 34 | // 35 | 36 | @inject public var easyAuthApi:EasyAuthApi; 37 | 38 | var modules:StringMap; 39 | 40 | /** 41 | Add a set of modules to the controller. 42 | 43 | This should be run as part of dependency injection. 44 | **/ 45 | @inject("", "adminModules") 46 | public function addModules( injector:Injector, moduleList:List> ) { 47 | modules = new StringMap(); 48 | for ( module in moduleList ) { 49 | var controller = injector.instantiate( module ); 50 | modules.set( controller.slug, controller ); 51 | } 52 | } 53 | 54 | // 55 | // Key Routes 56 | // 57 | 58 | @:route( "/login/", GET ) 59 | public function loginScreen():ActionResult { 60 | return drawLoginScreen( "" ); 61 | } 62 | 63 | @:route( "/login/", POST ) 64 | public function attemptLogin( args:{ user:String, pass:String } ):Surprise { 65 | return 66 | context.session.init() 67 | >> function( n:Noise ) return easyAuthApi.attemptLogin( args.user, args.pass ) 68 | >> function( outcome:Outcome ):Outcome { 69 | return switch outcome { 70 | case Success(user) if (passesAuth()): 71 | var r:ActionResult = new RedirectResult( baseUri ); 72 | Success( r ); 73 | case Success(user): 74 | // They're logged in, but don't have permission to be here. 75 | Failure( HttpError.authError(ANoPermission(UFAdminPermissions.UFACanAccessAdminArea)) ); 76 | case Failure(err): 77 | ufError( err, err.pos ); 78 | var r:ActionResult = drawLoginScreen( args.user ); 79 | Success( r ); 80 | } 81 | } 82 | } 83 | 84 | @:route( "/logout/" ) 85 | public function doLogout():ActionResult { 86 | easyAuthApi.logout(); 87 | return loginScreen(); 88 | } 89 | 90 | @:route("/") 91 | public function index():ActionResult { 92 | checkTablesExists(); 93 | if ( passesAuth() ) { 94 | 95 | var links:Array<{ slug:String, title:String }> = []; 96 | 97 | for ( module in modules ) 98 | links.push( module ); 99 | links.sort( function(l1,l2) return Reflect.compare(l1.title,l2.title) ); 100 | 101 | var template = CompileTime.readFile( "ufront/ufadmin/view/container.html" ); 102 | return UFAdminModule.wrapInLayout( "Ufront Admin Console", template, { links:links } ); 103 | } 104 | else { 105 | if (context.auth.isLoggedIn()) return throw HttpError.authError( ANoPermission(UFAdminPermissions.UFACanAccessAdminArea) ); 106 | else return loginScreen(); 107 | } 108 | } 109 | 110 | @:route( "/welcome/" ) 111 | public function welcomePage() { 112 | if ( passesAuth() ) { 113 | var template = CompileTime.readFile( "ufront/ufadmin/view/welcome.html" ); 114 | return UFAdminModule.wrapInLayout( "Ufront Admin Console", template, {} ); 115 | } 116 | else return throw HttpError.authError( ANoPermission(UFAdminPermissions.UFACanAccessAdminArea) ); 117 | } 118 | 119 | @:route( "/$module/*" ) 120 | public function doModule( module:String ):FutureActionOutcome { 121 | 122 | if ( passesAuth() ) { 123 | if ( modules.exists(module) ) { 124 | var controller = modules.get( module ); 125 | // Reset the baseUri... 126 | controller.injectContext( context ); 127 | return controller.execute(); 128 | } 129 | else return throw HttpError.pageNotFound(); 130 | } 131 | else return throw HttpError.authError( ANoPermission(UFAdminPermissions.UFACanAccessAdminArea) ); 132 | } 133 | 134 | // 135 | // Private 136 | // 137 | 138 | function checkTablesExists() { 139 | // if (!sys.db.TableCreate.exists(AdminTaskLog.manager)) sys.db.TableCreate.create(AdminTaskLog.manager); 140 | } 141 | 142 | function passesAuth():Bool { 143 | return context.auth.hasPermission( UFAdminPermissions.UFACanAccessAdminArea ); 144 | } 145 | 146 | function drawLoginScreen( existingUser:String ) { 147 | return new ViewResult({ 148 | existingUser: existingUser, 149 | }).usingTemplateString( 150 | CompileTime.readFile( "ufront/ufadmin/view/login.html" ), 151 | CompileTime.readFile( "ufront/ufadmin/view/layout.html" ) 152 | ); 153 | } 154 | } 155 | #end 156 | -------------------------------------------------------------------------------- /src/spadm/AdminStyle.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c)2012 Nicolas Cannasse 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining 5 | * a copy of this software and associated documentation files (the 6 | * "Software"), to deal in the Software without restriction, including 7 | * without limitation the rights to use, copy, modify, merge, publish, 8 | * distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so, subject to 10 | * the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be 13 | * included in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | package spadm; 24 | 25 | import haxe.macro.Context; 26 | #if neko 27 | import neko.Web; 28 | import neko.Lib; 29 | #elseif php 30 | import php.Web; 31 | import php.Lib; 32 | #end 33 | #if !macro 34 | import spadm.TableInfos.TableType; 35 | #end 36 | 37 | class MacroHelper { 38 | 39 | public macro static function importFile( file : String ) { 40 | var data = try sys.io.File.getContent(Context.resolvePath(file)) catch( e : Dynamic ) null; 41 | return Context.makeExpr(data,Context.currentPos()); 42 | } 43 | 44 | } 45 | 46 | #if !macro 47 | 48 | class AdminStyle { 49 | 50 | public static var BASE_URL = "/db/"; 51 | public static var CSS = { 52 | var file = MacroHelper.importFile("db.css"); 53 | if( file == null ) 54 | null 55 | else 56 | ''; 57 | } 58 | public static var HTML_BOTTOM = ""; 59 | 60 | var isNull : Bool; 61 | var value : String; 62 | var isHeader : Bool; 63 | var table : TableInfos; 64 | 65 | public function new(t) { 66 | this.table = t; 67 | } 68 | 69 | function out(str : String,?params : Dynamic) { 70 | if( params != null ) { 71 | for( x in Reflect.fields(params) ) 72 | str = str.split("@"+x).join(Reflect.field(params,x)); 73 | } 74 | Lib.println(str); 75 | } 76 | 77 | public function text(str,?title) { 78 | str = StringTools.htmlEscape(str); 79 | if( title != null ) str = ''+str+''; 80 | out(str); 81 | } 82 | 83 | public function begin( title ) { 84 | out('@title',{ title: title }); 85 | if( CSS != null ) 86 | out(CSS); 87 | out(''); 88 | out(' 89 | 98 | ',{ base : BASE_URL }); 99 | out(''); 100 | out('

@title

',{ title : title }); 101 | } 102 | 103 | public function end() { 104 | out('
'); 111 | out(HTML_BOTTOM); 112 | out(''); 113 | } 114 | 115 | public function beginList() { 116 | out("
    "); 117 | } 118 | 119 | public function endList() { 120 | out("
"); 121 | } 122 | 123 | public function beginItem() { 124 | out("
  • "); 125 | } 126 | 127 | public function endItem() { 128 | out("
  • "); 129 | } 130 | 131 | public function gotoURL(url) { 132 | Web.redirect(BASE_URL+url); 133 | } 134 | 135 | public function link( url, name ) { 136 | out('@name',{ url : BASE_URL+url, name : name }); 137 | } 138 | 139 | public function linkConfirm( url, name ) { 140 | out('@name',{ url : BASE_URL+url, name : name }); 141 | } 142 | 143 | public function beginForm(url,?file,?id) { 144 | out('
    ',{ id:id, url : BASE_URL+url, enc : if( file ) ' enctype="multipart/form-data"' else "" }); 145 | beginTable(); 146 | } 147 | 148 | public function endForm() { 149 | endTable(); 150 | out('
    '); 151 | } 152 | 153 | public function beginTable( ?css ) { 154 | if( css != null ) 155 | out('',{ css : css }); 156 | else 157 | out('
    '); 158 | } 159 | 160 | public function endTable() { 161 | out('
    '); 162 | } 163 | 164 | public function beginLine( ?isHeader, ?css ) { 165 | var str = '' else ''; 170 | out(str); 171 | this.isHeader = isHeader; 172 | } 173 | 174 | public function nextRow( ?isHeader ) { 175 | out((if( this.isHeader ) '' else '')+(if( isHeader ) '' else '')); 176 | this.isHeader = isHeader; 177 | } 178 | 179 | public function endLine() { 180 | out((if( this.isHeader ) '' else '')+''); 181 | } 182 | 183 | public function addSubmit( name, ?url, ?confirm, ?iname ) { 184 | beginLine(); 185 | nextRow(); 186 | out(''); 195 | endLine(); 196 | } 197 | 198 | public function checkBox(name,checked) { 199 | out(''); 203 | } 204 | 205 | function input(name,css,?options : Dynamic) { 206 | if( options == null ) 207 | options = {}; 208 | beginLine(true); 209 | out(name); 210 | nextRow(); 211 | if( isNull ) 212 | checkBox(name+"__data",value != null); 213 | out(''); 225 | endLine(); 226 | } 227 | 228 | function getFileURL( v : String ) { 229 | return "/file/" + v + ".png"; 230 | } 231 | 232 | function inputText(name, css, ?noWrap ) { 233 | beginLine(true); 234 | out(name); 235 | nextRow(); 236 | if( isNull ) 237 | checkBox(name+"__data",value != null); 238 | out('',{ noWrap : noWrap?' wrap="off"':'', name : name, css : css, value : if( value != null ) StringTools.htmlEscape(value) else "" }); 239 | endLine(); 240 | } 241 | 242 | public function inputField( name : String, type : TableType, isNull, value ) { 243 | this.isNull = isNull; 244 | this.value = value; 245 | switch( type ) { 246 | case DId, DUId, DBigId: 247 | infoField(name,if( value == null ) "#ID" else value); 248 | case DInt: 249 | input(name,"dint",{ size : 10 }); 250 | case DBigInt: 251 | input(name,"dbigint",{ size : 20 }); 252 | case DUInt: 253 | input(name,"duint",{ size : 10 }); 254 | case DTinyInt: 255 | input(name, "dtint", { size : 4 } ); 256 | case DTinyUInt, DSmallInt, DSmallUInt, DMediumInt, DMediumUInt: 257 | input(name, "dint", { size : 10 } ); 258 | case DFloat, DSingle: 259 | input(name,"dfloat",{ size : 10 }); 260 | case DBool: 261 | input(name,"dbool",{ isCheck : true }); 262 | case DString(n): 263 | input(name,"dstring",{ size : n }); 264 | case DTinyText: 265 | input(name,"dtinytext"); 266 | case DDate: 267 | if( value != null ) 268 | this.value = try value.toString().substr(0,10) catch( e : Dynamic ) "#INVALID"; 269 | input(name,"ddate",{ size : 10 }); 270 | case DDateTime, DTimeStamp: 271 | if( value != null ) 272 | this.value = try value.toString() catch( e : Dynamic ) "#INVALID"; 273 | input(name, "ddatetime", { size : 19 } ); 274 | case DText, DSmallText: 275 | inputText(name, "dtext"); 276 | case DSerialized, DNekoSerialized: 277 | inputText(name, "dtext", true); 278 | case DData: 279 | inputText(name, "dtext", true); 280 | case DEnum(_): 281 | // todo : use a select box with possible constructors 282 | input(name, "dtint", { size : 4 } ); 283 | case DEncoded: 284 | input(name,"denc",{ size : 6 }); 285 | case DFlags(fl,_): 286 | beginLine(true); 287 | out(name); 288 | nextRow(); 289 | if( isNull ) 290 | checkBox(name+"__data",value != null); 291 | var vint = Std.parseInt(value); 292 | if( vint == null ) vint = 0; 293 | var pos = 0; 294 | for( i in 0...fl.length ) { 295 | out(''); 299 | out(fl[i]); 300 | } 301 | endLine(); 302 | case DBinary, DSmallBinary, DLongBinary, DBytes(_), DNull, DInterval: 303 | throw "NotSupported"; 304 | } 305 | } 306 | 307 | public function binField( name : String, isNull, value : String, url : Void -> String ) { 308 | beginLine(true); 309 | out(name); 310 | nextRow(); 311 | if( isNull ) 312 | checkBox(name+"__data",value != null); 313 | if( value != null ) 314 | text("["+value.length+" bytes]"); 315 | else if( url != null ) 316 | text("null"); 317 | out('',{ name : name }); 318 | if( value != null && url != null ) 319 | link(url(),"download"); 320 | endLine(); 321 | } 322 | 323 | public function infoField( name : String, value ) { 324 | beginLine(true); 325 | out(name); 326 | nextRow(); 327 | out(value); 328 | endLine(); 329 | } 330 | 331 | public function choiceField( name : String, values : List<{ id : String, str : String }>, def : String, link, ?disabled : Bool, ?isImage: Bool ) { 332 | beginLine(true); 333 | out(name); 334 | nextRow(); 335 | var infos = { 336 | func : if( isImage ) "updateImage" else "updateLink", 337 | name : name, 338 | link : link, 339 | size : if( values != null && values.length > 15 ) 10 else 1, 340 | dis : if( disabled ) 'disabled="yes"' else "", 341 | def : if( def == "null" ) "" else def, 342 | }; 343 | if( values == null ) 344 | out('',infos); 345 | else { 346 | out(''); 351 | } 352 | out('goto',{ name : name }); 353 | if( isImage ) 354 | out('',{ name : name, file : getFileURL(def) }); 355 | out('',{ name : name }); 356 | endLine(); 357 | } 358 | 359 | public function errorField( message ) { 360 | beginLine(true); 361 | nextRow(); 362 | error(message); 363 | endLine(); 364 | } 365 | 366 | public function error( message ) { 367 | out('
    @msg
    ',{ msg : message }); 368 | } 369 | 370 | } 371 | 372 | #end 373 | -------------------------------------------------------------------------------- /src/spadm/Serialized.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c)2012 Nicolas Cannasse 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining 5 | * a copy of this software and associated documentation files (the 6 | * "Software"), to deal in the Software without restriction, including 7 | * without limitation the rights to use, copy, modify, merge, publish, 8 | * distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so, subject to 10 | * the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be 13 | * included in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | package spadm; 24 | 25 | #if hscript 26 | import hscript.Expr; 27 | #end 28 | 29 | private enum Errors { 30 | Invalid; 31 | } 32 | 33 | private typedef Current = { 34 | var old : Current; 35 | var lines : Array; 36 | var totalSize : Int; 37 | var maxSize : Int; 38 | var prefix : String; 39 | var sep : String; 40 | var buf : StringBuf; 41 | } 42 | 43 | class Serialized { 44 | 45 | var value : String; 46 | var pos : Int; 47 | var buf : StringBuf; 48 | var shash : Map; 49 | var scount : Int; 50 | var scache : Array; 51 | var useEnumIndex : Bool; 52 | 53 | var cur : Current; 54 | var tabs : Int; 55 | 56 | static var IDENT = " "; 57 | static var ident = ~/^[A-Za-z_][A-Za-z0-9_]*$/; 58 | static var clname = ~/^[A-Za-z_][A-Z.a-z0-9_]*$/; 59 | static var BASE64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789%:"; 60 | 61 | public function new(v) { 62 | this.value = v; 63 | pos = 0; 64 | tabs = 0; 65 | } 66 | 67 | public function encode() : String { 68 | #if !hscript 69 | throw "You can't edit this without -lib hscript"; 70 | return null; 71 | #else 72 | if( value == "" ) 73 | return ""; 74 | var p = new hscript.Parser(); 75 | p.allowJSON = true; 76 | var e = p.parse(new haxe.io.StringInput(value)); 77 | buf = new StringBuf(); 78 | shash = new Map(); 79 | scount = 0; 80 | encodeRec(e); 81 | return buf.toString(); 82 | #end 83 | } 84 | 85 | #if hscript 86 | 87 | function getString( e : Expr ) { 88 | return (e == null) ? null : switch( e ) { 89 | case EConst(v): 90 | switch(v) { 91 | case CString(s): s; 92 | default: null; 93 | } 94 | default: null; 95 | }; 96 | } 97 | 98 | function getPath( e : Expr ) { 99 | if( e == null ) 100 | return null; 101 | switch( e ) { 102 | case EConst(v): 103 | return switch(v) { 104 | case CString(s): s; 105 | default: null; 106 | } 107 | case EIdent(v): 108 | return v; 109 | case EField(e, f): 110 | var path = "." + f; 111 | while( true ) { 112 | switch( e ) { 113 | case EIdent(i): return i + path; 114 | case EField(p, f): path = "." + f + path; e = p; 115 | default: return null; 116 | } 117 | } 118 | default: 119 | } 120 | return null; 121 | } 122 | 123 | function encodeRec( e : hscript.Expr ) { 124 | switch( e ) { 125 | case EConst(v): 126 | switch(v) { 127 | case CString(s): 128 | encodeString(s); 129 | case CInt(v): 130 | if( v == 0 ) { 131 | buf.add("z"); 132 | return; 133 | } 134 | buf.add("i"); 135 | buf.add(v); 136 | case CFloat(v): 137 | if( Math.isNaN(v) ) 138 | buf.add("k"); 139 | else if( !Math.isFinite(v) ) 140 | buf.add(if( v < 0 ) "m" else "p"); 141 | else { 142 | buf.add("d"); 143 | buf.add(v); 144 | } 145 | #if !haxe3 146 | case CInt32(i): 147 | buf.add("d"); 148 | buf.add(v); 149 | #end 150 | } 151 | case EUnop(op, _, es): 152 | if( op == "-" ) 153 | switch( es ) { 154 | case EConst(v): 155 | switch(v) { 156 | case CInt(i): 157 | encodeRec(EConst(CInt(-i))); 158 | return; 159 | case CFloat(f): 160 | encodeRec(EConst(CFloat(-f))); 161 | return; 162 | default: 163 | } 164 | default: 165 | } 166 | throw "Unsupported " + Type.enumConstructor(e); 167 | case EIdent(v): 168 | switch( v ) { 169 | case "null": 170 | buf.add("n"); 171 | case "true": 172 | buf.add("t"); 173 | case "false": 174 | buf.add("f"); 175 | case "NaN": 176 | buf.add("k"); 177 | case "Inf": 178 | buf.add("p"); 179 | case "NegInf": 180 | buf.add("m"); 181 | default: 182 | throw "Unknown identifier " + v; 183 | } 184 | case EArrayDecl(el): 185 | var ucount = 0; 186 | buf.add("a"); 187 | for( e in el ) { 188 | switch( e ) { 189 | case EIdent(i): 190 | if( i == "null" ) { 191 | ucount++; 192 | continue; 193 | } 194 | default: 195 | } 196 | if( ucount > 0 ) { 197 | if( ucount == 1 ) 198 | buf.add("n"); 199 | else { 200 | buf.add("u"); 201 | buf.add(ucount); 202 | } 203 | ucount = 0; 204 | } 205 | encodeRec(e); 206 | } 207 | if( ucount > 0 ) { 208 | if( ucount == 1 ) 209 | buf.add("n"); 210 | else { 211 | buf.add("u"); 212 | buf.add(ucount); 213 | } 214 | } 215 | buf.add("h"); 216 | case EObject(fields): 217 | buf.add("o"); 218 | for( f in fields ) { 219 | encodeString(f.name); 220 | encodeRec(f.e); 221 | } 222 | buf.add("g"); 223 | case ECall(e, params): 224 | switch( e ) { 225 | case EIdent(call): 226 | switch(call) { 227 | case "empty": 228 | if( params.length == 0 ) 229 | return; 230 | case "invalid": 231 | var str = getString(params[0]); 232 | if( params.length == 1 && str != null ) { 233 | buf.add(str); 234 | return; 235 | } 236 | case "list": 237 | buf.add("l"); 238 | for( e in params ) 239 | encodeRec(e); 240 | buf.add("h"); 241 | return; 242 | case "date": 243 | var str = getString(params[0]); 244 | // check format 245 | if( params.length == 1 && str != null ) { 246 | var d = Date.fromString(str); 247 | buf.add("v"); 248 | buf.add(d.toString()); 249 | return; 250 | } 251 | case "now": 252 | if( params.length == 0 ) { 253 | buf.add("v"); 254 | buf.add(Date.now()); 255 | return; 256 | } 257 | case "error": 258 | if( params.length == 1 ) { 259 | buf.add("x"); 260 | encodeRec(params[0]); 261 | return; 262 | } 263 | case "hash": 264 | if( params.length == 1 ) 265 | switch( params[0] ) { 266 | case EObject(fields): 267 | buf.add("b"); 268 | for( f in fields ) { 269 | encodeString(f.name); 270 | encodeRec(f.e); 271 | } 272 | buf.add("h"); 273 | return; 274 | default: 275 | } 276 | case "inthash": 277 | if( params.length == 1 ) 278 | switch( params[0] ) { 279 | case EObject(fields): 280 | buf.add("q"); 281 | for( f in fields ) { 282 | if( !~/^-?[0-9]+$/.match(f.name) ) 283 | throw "Invalid IntHash key '"+f.name+"'"; 284 | buf.add(":"); 285 | buf.add(f.name); 286 | encodeRec(f.e); 287 | } 288 | buf.add("h"); 289 | return; 290 | default: 291 | } 292 | case "bytes": 293 | var str = getString(params[0]); 294 | if( params.length == 1 && str != null ) { 295 | for( i in 0...str.length ) 296 | if( BASE64.indexOf(str.charAt(i)) == -1 ) 297 | throw "Invalid Base64 char"; 298 | buf.add("s"); 299 | buf.add(str.length); 300 | buf.add(":"); 301 | buf.add(str); 302 | return; 303 | } 304 | case "indexes": 305 | if( params.length == 1 ) { 306 | useEnumIndex = true; 307 | encodeRec(params[0]); 308 | return; 309 | } 310 | case "ref": 311 | if( params.length == 1 ) { 312 | switch( params[0] ) { 313 | case EConst(v): 314 | switch(v) { 315 | case CInt(i): 316 | buf.add("r"); 317 | buf.add(i); 318 | return; 319 | default: 320 | } 321 | default: 322 | } 323 | } 324 | default: 325 | throw "Unsupported call '"+call+"'"; 326 | } 327 | case EField(e, f): 328 | encodeEnum(e, f, params); 329 | return; 330 | case EArray(e, index): 331 | encodeEnum(e, index, params); 332 | return; 333 | default: 334 | } 335 | throw "Unsupported call"; 336 | case EField(e, f): 337 | encodeEnum(e, f, []); 338 | case EArray(e,index): 339 | encodeEnum(e, index, []); 340 | case ENew(c, params): 341 | var fields = null, cname = null; 342 | if( c == "class" ) { 343 | if( params.length == 2 ) { 344 | cname = getString(params[0]); 345 | fields = switch( params[1] ) { case EObject(fields): fields; default : null; } 346 | } 347 | } else if( c == "custom" ) { 348 | cname = getPath(params[0]); 349 | if( cname != null ) { 350 | buf.add("C"); 351 | encodeString(cname); 352 | for( i in 1...params.length ) 353 | encodeRec(params[i]); 354 | buf.add("g"); 355 | return; 356 | } 357 | } else { 358 | if( params.length == 1 ) { 359 | cname = c; 360 | fields = switch( params[0] ) { case EObject(fields): fields; default : null; } 361 | } 362 | } 363 | if( cname == null || fields == null ) 364 | throw "Invalid 'new'"; 365 | buf.add("c"); 366 | encodeString(cname); 367 | for( f in fields ) { 368 | encodeString(f.name); 369 | encodeRec(f.e); 370 | } 371 | buf.add("g"); 372 | default: 373 | throw "Unsupported " + Type.enumConstructor(e); 374 | } 375 | } 376 | 377 | function encodeEnum( e : Expr, ?name : String, ?eindex : Expr, args : Array ) { 378 | var ename = getPath(e); 379 | if( ename == null ) 380 | throw "Invalid enum path"; 381 | var index : Null = null; 382 | if( eindex != null ) { 383 | switch( eindex ) { 384 | case EConst(c): 385 | switch( c ) { 386 | case CInt(i): index = i; 387 | case CString(s): name = s; 388 | default: 389 | } 390 | default: 391 | } 392 | if( index == null && name == null ) throw "Invalid enum index"; 393 | } 394 | if( name != null ) { 395 | var e = try Type.resolveEnum(ename) catch( e : Dynamic ) null; 396 | if( e == null ) { 397 | if( useEnumIndex ) 398 | throw "Unknown enum '" + ename + "' : use index"; 399 | } else { 400 | index = Lambda.indexOf(Type.getEnumConstructs(e), name); 401 | if( index < 0 ) throw name + " is not part of enum " + ename + "(" + Type.getEnumConstructs(e).join(",") + ")"; 402 | if( useEnumIndex ) name = null else index = null; 403 | } 404 | } 405 | buf.add((index != null)?"j":"w"); 406 | encodeString(ename); 407 | if( index != null ) { 408 | buf.add(":"); 409 | buf.add(index); 410 | } else 411 | encodeString(name); 412 | buf.add(":"); 413 | buf.add(args.length); 414 | for( a in args ) 415 | encodeRec(a); 416 | } 417 | 418 | function encodeString( s : String ) { 419 | var x = shash.get(s); 420 | if( x != null ) { 421 | buf.add("R"); 422 | buf.add(x); 423 | return; 424 | } 425 | shash.set(s,scount++); 426 | buf.add("y"); 427 | s = StringTools.urlEncode(s); 428 | buf.add(s.length); 429 | buf.add(":"); 430 | buf.add(s); 431 | } 432 | #end 433 | 434 | function quote( s : String, ?r : EReg ) { 435 | if( r != null && r.match(s) ) 436 | return s; 437 | return "'"+s.split("\\").join("\\\\").split("'").join("\\'").split("\n").join("\\n").split("\r").join("\\r").split("\t").join("\\t")+"'"; 438 | } 439 | 440 | public function escape() { 441 | if( value == "" ) 442 | return "empty()"; 443 | buf = new StringBuf(); 444 | scache = new Array(); 445 | try loop() catch( e : Errors ) pos = -1; 446 | if( pos != value.length ) 447 | return "invalid(" + quote(value) + ")"; 448 | var str = buf.toString(); 449 | if( useEnumIndex ) 450 | str = "indexes(" + str + ")"; 451 | return str; 452 | } 453 | 454 | inline function get(pos) { 455 | return value.charCodeAt(pos); 456 | } 457 | 458 | function readDigits() { 459 | var k = 0; 460 | var s = false; 461 | var fpos = pos; 462 | while( true ) { 463 | var c = get(pos); 464 | if( c == null ) 465 | break; 466 | if( c == "-".code ) { 467 | if( pos != fpos ) 468 | break; 469 | s = true; 470 | pos++; 471 | continue; 472 | } 473 | if( c < "0".code || c > "9".code ) 474 | break; 475 | k = k * 10 + (c - "0".code); 476 | pos++; 477 | } 478 | if( s ) 479 | k *= -1; 480 | return k; 481 | } 482 | 483 | function loop() { 484 | switch( get(pos++) ) { 485 | case "n".code: 486 | buf.add(null); 487 | case "i".code: 488 | buf.add(readDigits()); 489 | case "z".code: 490 | buf.add(0); 491 | case "t".code: 492 | buf.add(true); 493 | case "f".code: 494 | buf.add(false); 495 | case "k".code: 496 | buf.add("NaN"); 497 | case "p".code: 498 | buf.add("Inf"); 499 | case "m".code: 500 | buf.add("NegInf"); 501 | case "d".code: 502 | var p1 = pos; 503 | while( true ) { 504 | var c = get(pos); 505 | // + - . , 0-9 506 | if( (c >= 43 && c < 58) || c == "e".code || c == "E".code ) 507 | pos++; 508 | else 509 | break; 510 | } 511 | buf.add(value.substr(p1, pos - p1)); 512 | case "a".code: 513 | open("[",", "); 514 | while( true ) { 515 | var c = get(pos); 516 | if( c == "h".code ) { 517 | pos++; 518 | break; 519 | } 520 | if( c == "u".code ) { 521 | pos++; 522 | for( i in 0...readDigits() - 1 ) { 523 | buf.add("null"); 524 | next(); 525 | } 526 | buf.add("null"); 527 | } else 528 | loop(); 529 | next(); 530 | } 531 | close("]"); 532 | case "y".code, "R".code: 533 | pos--; 534 | buf.add(quote(readString())); 535 | case "l".code: 536 | open("list(",", "); 537 | while( get(pos) != "h".code ) { 538 | loop(); 539 | next(); 540 | } 541 | close(")"); 542 | pos++; 543 | case "v".code: 544 | buf.add("date("); 545 | buf.add(quote(value.substr(pos, 19))); 546 | buf.add(")"); 547 | pos += 19; 548 | case "x".code: 549 | buf.add("error("); 550 | loop(); 551 | buf.add(")"); 552 | case "o".code: 553 | loopObj("g".code); 554 | case "b".code: 555 | buf.add("hash("); 556 | loopObj("h".code); 557 | buf.add(")"); 558 | case "q".code: 559 | buf.add("inthash("); 560 | open("{",", "," "); 561 | var c = get(pos++); 562 | while( c == ":".code ) { 563 | buf.add("'"+readDigits()+"'"); 564 | buf.add(" : "); 565 | loop(); 566 | c = get(pos++); 567 | next(); 568 | } 569 | if( c != "h".code ) 570 | throw Invalid; 571 | close("}", " "); 572 | buf.add(")"); 573 | case "s".code: 574 | var len = readDigits(); 575 | if( get(pos++) != ":".code || value.length - pos < len ) 576 | throw Invalid; 577 | buf.add("bytes("); 578 | buf.add(quote(value.substr(pos, len))); 579 | buf.add(")"); 580 | pos += len; 581 | case "w".code: 582 | buf.add(quote(readString(), clname)); 583 | var constr = readString(); 584 | if( ident.match(constr) ) 585 | buf.add("." + constr); 586 | else 587 | buf.add("["+quote(constr)+"]"); 588 | if( get(pos++) != ":".code ) 589 | throw Invalid; 590 | var nargs = readDigits(); 591 | if( nargs > 0 ) { 592 | buf.add("("); 593 | for( i in 0...nargs ) { 594 | if( i > 0 ) buf.add(", "); 595 | loop(); 596 | } 597 | buf.add(")"); 598 | } 599 | case "j".code: 600 | var cl = readString(); 601 | buf.add(quote(cl, clname)); 602 | if( get(pos++) != ":".code ) 603 | throw Invalid; 604 | var index = readDigits(); 605 | var e = Type.resolveEnum(cl); 606 | if( e == null ) 607 | buf.add("["+index+"]"); 608 | else { 609 | useEnumIndex = true; 610 | buf.add("."+Type.getEnumConstructs(e)[index]); 611 | } 612 | if( get(pos++) != ":".code ) 613 | throw Invalid; 614 | var nargs = readDigits(); 615 | if( nargs > 0 ) { 616 | buf.add("("); 617 | for( i in 0...nargs ) { 618 | if( i > 0 ) buf.add(", "); 619 | loop(); 620 | } 621 | buf.add(")"); 622 | } 623 | case "c".code: 624 | buf.add("new "); 625 | var cl = readString(); 626 | if( clname.match(cl) ) 627 | buf.add(cl + "("); 628 | else { 629 | buf.add("class("); 630 | buf.add(quote(cl)); 631 | buf.add(","); 632 | } 633 | loopObj("g".code); 634 | buf.add(")"); 635 | case "C".code: 636 | open("new custom(",", "); 637 | buf.add(quote(readString(), clname)); 638 | next(); 639 | while( get(pos) != "g".code ) { 640 | loop(); 641 | next(); 642 | } 643 | close(")"); 644 | pos++; 645 | case "r".code: 646 | buf.add("ref("+readDigits()+")"); 647 | default: 648 | throw Invalid; 649 | } 650 | } 651 | 652 | function readString() : String { 653 | switch( value.charCodeAt(pos++) ) { 654 | case "y".code: 655 | var len = readDigits(); 656 | if( get(pos++) != ":".code || value.length - pos < len ) 657 | throw Invalid; 658 | var s = value.substr(pos,len); 659 | pos += len; 660 | s = StringTools.urlDecode(s); 661 | scache.push(s); 662 | return s; 663 | case "R".code: 664 | var n = readDigits(); 665 | if( n < 0 || n >= scache.length ) 666 | throw "Invalid string reference"; 667 | return scache[n]; 668 | default: 669 | throw Invalid; 670 | } 671 | } 672 | 673 | function loopObj(eof) { 674 | open("{",", "," "); 675 | while( true ) { 676 | if( pos >= value.length ) 677 | throw Invalid; 678 | if( get(pos) == eof ) 679 | break; 680 | buf.add(quote(readString(), ident)); 681 | buf.add(" : "); 682 | loop(); 683 | next(); 684 | } 685 | close("}"," "); 686 | pos++; 687 | } 688 | 689 | function open(str, sep, ?prefix) { 690 | buf.add(str); 691 | tabs++; 692 | cur = { old : cur, sep : sep, prefix : prefix, lines : [], buf : buf, totalSize : 0, maxSize : 0 }; 693 | buf = new StringBuf(); 694 | } 695 | 696 | function next() { 697 | var line = buf.toString(); 698 | if( line.length > cur.maxSize ) cur.maxSize = line.length; 699 | cur.totalSize += line.length; 700 | cur.lines.push(line); 701 | buf = new StringBuf(); 702 | } 703 | 704 | function close(end,?postfix) { 705 | buf = cur.buf; 706 | var t = "\n"; 707 | for( i in 0...tabs-1 ) 708 | t += IDENT; 709 | if( t.length + cur.totalSize > 80 && cur.maxSize > 10 ) { 710 | var first = true; 711 | for( line in cur.lines ) { 712 | if( first ) first = false else buf.add(cur.sep); 713 | buf.add(t + IDENT + line); 714 | } 715 | buf.add(t); 716 | buf.add(end); 717 | } else { 718 | if( cur.prefix != null && cur.lines.length > 0 ) buf.add(cur.prefix); 719 | var first = true; 720 | for( line in cur.lines ) { 721 | if( first ) first = false else buf.add(cur.sep); 722 | buf.add(line); 723 | } 724 | if( !first && postfix != null ) buf.add(postfix); 725 | buf.add(end); 726 | } 727 | cur = cur.old; 728 | tabs--; 729 | } 730 | 731 | } -------------------------------------------------------------------------------- /src/spadm/TableInfos.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c)2012 Nicolas Cannasse 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining 5 | * a copy of this software and associated documentation files (the 6 | * "Software"), to deal in the Software without restriction, including 7 | * without limitation the rights to use, copy, modify, merge, publish, 8 | * distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so, subject to 10 | * the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be 13 | * included in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | package spadm; 24 | 25 | import sys.db.Object; 26 | import sys.db.Manager; 27 | 28 | typedef TableType = sys.db.RecordInfos.RecordType; 29 | 30 | typedef ManagerAccess = { 31 | private var table_name : String; 32 | private var table_keys : Array; 33 | private function quote( v : Dynamic ) : String; 34 | private function quoteField( f : String ) : String; 35 | private function addKeys( s : StringBuf, x : {} ) : Void; 36 | function all( ?lock : Bool ) : List; 37 | function dbClass() : Class; 38 | } 39 | 40 | private typedef TableRelation = { 41 | var prop : String; 42 | var key : String; 43 | var lock : Bool; 44 | var manager : ManagerAccess; 45 | var className : String; 46 | var cascade : Bool; 47 | } 48 | 49 | class TableInfos { 50 | 51 | public static var ENGINE = "InnoDB"; 52 | public static var OLD_COMPAT = false; // only set for old DBs ! 53 | 54 | public var primary(default,null) : List; 55 | public var cl(default,null) : Class; 56 | public var name(default,null) : String; 57 | public var className(default,null) : String; 58 | public var hfields(default,null) : Map; 59 | public var fields(default,null) : List<{ name : String, type : TableType }>; 60 | public var nulls(default,null) : Map; 61 | public var relations(default,null) : Array; 62 | public var indexes(default,null) : List<{ keys : List, unique : Bool }>; 63 | public var manager : Manager; 64 | 65 | public function new( cname : String ) { 66 | hfields = new Map(); 67 | fields = new List(); 68 | nulls = new Map(); 69 | cl = cast Type.resolveClass("db."+cname); 70 | if( cl == null ) 71 | cl = cast Type.resolveClass(cname); 72 | else 73 | cname = "db."+cname; 74 | if( cl == null ) 75 | throw "Class not found : "+cname; 76 | manager = untyped cl.manager; 77 | if( manager == null ) 78 | throw "No static manager for "+cname; 79 | className = cname; 80 | if( className.substr(0,3) == "db." ) className = className.substr(3); 81 | var a = cname.split("."); 82 | name = a.pop(); 83 | processClass(); 84 | } 85 | 86 | function processClass() { 87 | var rtti = haxe.rtti.Meta.getType(cl).rtti; 88 | if( rtti == null ) 89 | throw "Class "+name+" does not have RTTI"; 90 | var infos : sys.db.RecordInfos = haxe.Unserializer.run(rtti[0]); 91 | name = infos.name; 92 | primary = Lambda.list(infos.key); 93 | for( f in infos.fields ) { 94 | fields.add({ name : f.name, type : f.t }); 95 | hfields.set(f.name, f.t); 96 | if( f.isNull ) nulls.set(f.name, true); 97 | } 98 | relations = new Array(); 99 | for( r in infos.relations ) { 100 | var t = Type.resolveClass(r.type); 101 | if( t == null ) throw "Missing type " + r.type + " for relation " + name + "." + r.prop; 102 | var manager : ManagerAccess = Reflect.field(t, "manager"); 103 | if( manager == null ) throw r.type + " does not have a static field manager"; 104 | relations.push( { prop : r.prop, key : r.key, lock : r.lock, manager : manager, className : Type.getClassName(manager.dbClass()), cascade : r.cascade } ); 105 | } 106 | indexes = new List(); 107 | for( i in infos.indexes ) 108 | indexes.push( { keys : Lambda.list(i.keys), unique : i.unique } ); 109 | } 110 | 111 | function escape( name : String ) { 112 | var m : ManagerAccess = manager; 113 | return m.quoteField(name); 114 | } 115 | 116 | public static function unescape( field : String ) { 117 | if( field.length > 1 && field.charAt(0) == '`' && field.charAt(field.length-1) == '`' ) 118 | return field.substr(1,field.length-2); 119 | return field; 120 | } 121 | 122 | public function isRelationActive( r : Dynamic ) { 123 | return true; 124 | } 125 | 126 | public function createRequest( full : Bool ) { 127 | var str = "CREATE TABLE "+escape(name)+" (\n"; 128 | var keys = fields.iterator(); 129 | for( f in keys ) { 130 | str += escape(f.name)+" "+fieldInfos(f); 131 | if( keys.hasNext() ) 132 | str += ","; 133 | str += "\n"; 134 | } 135 | if( primary != null ) 136 | str += ", PRIMARY KEY ("+primary.map(escape).join(",")+")\n"; 137 | if( full ) { 138 | for( r in relations ) 139 | if( isRelationActive(r) ) 140 | str += ", "+relationInfos(r); 141 | for( i in indexes ) 142 | str += ", "+(if( i.unique ) "UNIQUE " else "")+"KEY "+escape(name+"_"+i.keys.join("_"))+"("+i.keys.map(escape).join(",")+")\n"; 143 | } 144 | str += ")"; 145 | if( ENGINE != null ) 146 | str += " ENGINE="+ENGINE; 147 | return str; 148 | } 149 | 150 | function relationInfos(r : TableRelation) { 151 | if( r.manager.table_keys.length != 1 ) 152 | throw "Relation on a multiple-keys table"; 153 | var rq = "CONSTRAINT "+escape(name+"_"+r.prop)+" FOREIGN KEY ("+escape(r.key)+") REFERENCES "+escape(r.manager.table_name)+"("+escape(r.manager.table_keys[0])+") "; 154 | rq += "ON DELETE "+(if( nulls.get(r.key) && r.cascade != true ) "SET NULL" else "CASCADE")+"\n"; 155 | return rq; 156 | } 157 | 158 | function fieldInfos(f) { 159 | return (switch( f.type ) { 160 | case DId: "INT AUTO_INCREMENT"; 161 | case DUId: "INT UNSIGNED AUTO_INCREMENT"; 162 | case DInt, DEncoded: "INT"; 163 | case DFlags(fl, auto): auto ? (fl.length <= 8 ? "TINYINT UNSIGNED" : (fl.length <= 16 ? "SMALLINT UNSIGNED" : (fl.length <= 24 ? "MEDIUMINT UNSIGNED" : "INT"))) : "INT"; 164 | case DTinyInt: "TINYINT"; 165 | case DUInt: "INT UNSIGNED"; 166 | case DSingle: "FLOAT"; 167 | case DFloat: "DOUBLE"; 168 | case DBool: "TINYINT(1)"; 169 | case DString(n): "VARCHAR("+n+")"; 170 | case DDate: "DATE"; 171 | case DDateTime: "DATETIME"; 172 | case DTimeStamp: "TIMESTAMP"+(nulls.exists(f.name) ? " NULL DEFAULT NULL" : " DEFAULT 0"); 173 | case DTinyText: "TINYTEXT"; 174 | case DSmallText: "TEXT"; 175 | case DText, DSerialized: "MEDIUMTEXT"; 176 | case DSmallBinary: "BLOB"; 177 | case DBinary, DNekoSerialized: "MEDIUMBLOB"; 178 | case DData: "MEDIUMBLOB"; 179 | case DEnum(_): "TINYINT UNSIGNED"; 180 | case DLongBinary: "LONGBLOB"; 181 | case DBigInt: "BIGINT"; 182 | case DBigId: "BIGINT AUTO_INCREMENT"; 183 | case DBytes(n): "BINARY(" + n + ")"; 184 | case DTinyUInt: "TINYINT UNSIGNED"; 185 | case DSmallInt: "SMALLINT"; 186 | case DSmallUInt: "SMALLINT UNSIGNED"; 187 | case DMediumInt: "MEDIUMINT"; 188 | case DMediumUInt: "MEDIUMINT UNSIGNED"; 189 | case DNull, DInterval: throw "assert"; 190 | }) + if( nulls.exists(f.name) ) "" else " NOT NULL"; 191 | } 192 | 193 | public function dropRequest() { 194 | return "DROP TABLE "+escape(name); 195 | } 196 | 197 | public function truncateRequest() { 198 | return "TRUNCATE TABLE "+escape(name); 199 | } 200 | 201 | public function descriptionRequest() { 202 | return "SHOW CREATE TABLE "+escape(name); 203 | } 204 | 205 | public function existsRequest() { 206 | return "SELECT * FROM "+escape(name)+" LIMIT 0"; 207 | } 208 | 209 | public static function countRequest( m : ManagerAccess, max : Int ) { 210 | return "SELECT " + m.quoteField(m.table_keys[0]) + " FROM " + m.quoteField(m.table_name) + " LIMIT " + max; 211 | } 212 | 213 | public function addFieldRequest( fname : String ) { 214 | var ftype = hfields.get(fname); 215 | if( ftype == null ) 216 | throw "No field "+fname; 217 | var rq = "ALTER TABLE "+escape(name)+" ADD "; 218 | return rq + escape(fname)+" "+fieldInfos({ name : fname, type : ftype }); 219 | } 220 | 221 | public function removeFieldRequest( fname : String ) { 222 | return "ALTER TABLE "+escape(name)+" DROP "+escape(fname); 223 | } 224 | 225 | public function renameFieldRequest( old : String, newname : String ) { 226 | var ftype = hfields.get(newname); 227 | if( ftype == null ) 228 | throw "No field "+newname; 229 | var rq = "ALTER TABLE "+escape(name)+" CHANGE "+escape(old)+" "; 230 | return rq + escape(newname) + " " + fieldInfos({ name : newname, type : ftype }); 231 | } 232 | 233 | public function updateFieldRequest( fname : String ) { 234 | var ftype = hfields.get(fname); 235 | if( ftype == null ) 236 | throw "No field "+fname; 237 | var rq = "ALTER TABLE "+escape(name)+" MODIFY "; 238 | return rq + escape(fname)+" "+fieldInfos({ name : fname, type : ftype }); 239 | } 240 | 241 | public function addRelationRequest( key : String, prop : String ) { 242 | for( r in relations ) 243 | if( r.key == key && r.prop == prop ) 244 | return "ALTER TABLE "+escape(name)+" ADD "+relationInfos(r); 245 | return throw "No such relation : "+prop+"("+key+")"; 246 | } 247 | 248 | public function deleteRelationRequest( rel : String ) { 249 | return "ALTER TABLE "+escape(name)+" DROP FOREIGN KEY "+escape(rel); 250 | } 251 | 252 | public function indexName( idx : Array ) { 253 | return name+"_"+idx.join("_"); 254 | } 255 | 256 | public function addIndexRequest( idx : Array, unique : Bool ) { 257 | var eidx = new Array(); 258 | for( i in idx ) { 259 | var k = escape(i); 260 | var f = hfields.get(i); 261 | if( f != null ) 262 | switch( f ) { 263 | case DTinyText, DSmallText, DText, DSmallBinary, DLongBinary, DBinary: 264 | k += "(4)"; // index size 265 | default: 266 | } 267 | eidx.push(k); 268 | } 269 | return "ALTER TABLE "+escape(name)+" ADD "+(if( unique ) "UNIQUE " else "")+"INDEX "+escape(indexName(idx))+"("+eidx.join(",")+")"; 270 | } 271 | 272 | public function deleteIndexRequest( idx : String ) { 273 | return "ALTER TABLE "+escape(name)+" DROP INDEX "+escape(idx); 274 | } 275 | 276 | public function updateFields( o : {}, fields : List<{ name : String, value : Dynamic }> ) { 277 | var me = this; 278 | var s = new StringBuf(); 279 | s.add("UPDATE "); 280 | s.add(escape(name)); 281 | s.add(" SET "); 282 | var first = true; 283 | for( f in fields ) { 284 | if( first ) 285 | first = false; 286 | else 287 | s.add(", "); 288 | s.add(escape(f.name)); 289 | s.add(" = "); 290 | Manager.cnx.addValue(s,f.value); 291 | } 292 | s.add(" WHERE "); 293 | var m : ManagerAccess = manager; 294 | m.addKeys(s,o); 295 | return s.toString(); 296 | } 297 | 298 | public function identifier( o : Object ) : String { 299 | if( primary == null ) 300 | throw "No primary key"; 301 | return primary.map(function(p) { return Std.string(Reflect.field(o,p)).split(".").join("~"); }).join("@"); 302 | } 303 | 304 | public function fromIdentifier( id : String ) : Object { 305 | var ids = id.split("@"); 306 | if( primary == null ) 307 | throw "No primary key"; 308 | if( ids.length != primary.length ) 309 | throw "Invalid identifier"; 310 | var keys = {}; 311 | for( p in primary ) 312 | Reflect.setField(keys, p, makeNativeValue(hfields.get(p), ids.shift().split("~").join("."))); 313 | return manager.unsafeGetWithKeys(keys); 314 | } 315 | 316 | function makeNativeValue( t : TableType, v : String ) : Dynamic { 317 | return switch( t ) { 318 | case DInt, DUInt, DId, DUId, DEncoded, DFlags(_), DTinyInt: cast Std.parseInt(v); 319 | case DTinyUInt, DSmallInt, DSmallUInt, DMediumUInt, DMediumInt: cast Std.parseInt(v); 320 | case DFloat, DSingle, DBigInt, DBigId: cast Std.parseFloat(v); 321 | case DDate, DDateTime, DTimeStamp: cast Date.fromString(v); 322 | case DBool: cast (v == "true"); 323 | case DText, DString(_), DSmallText, DTinyText, DBinary, DSmallBinary, DLongBinary, DSerialized, DNekoSerialized, DBytes(_): cast v; 324 | case DData: cast v; 325 | case DEnum(_): cast v; 326 | case DNull, DInterval: throw "assert"; 327 | }; 328 | } 329 | 330 | public function fromSearch( params : Map, order : String, pos : Int, count : Int ) : List { 331 | var rop = ~/^([<>]=?)(.+)$/; 332 | var cond = "TRUE"; 333 | var m : ManagerAccess = manager; 334 | for( p in params.keys() ) { 335 | var f = hfields.get(p); 336 | var v = params.get(p); 337 | if( f == null ) 338 | continue; 339 | cond += " AND " + escape(p); 340 | if( v == null || v == "NULL" ) 341 | cond += " IS NULL"; 342 | else switch( f ) { 343 | case DEncoded: 344 | cond += " = "+(try Id.encode(v) catch( e : Dynamic ) 0); 345 | case DString(_),DTinyText,DSmallText,DText: 346 | cond += " LIKE "+m.quote(v); 347 | case DBool: 348 | cond += " = "+((v == "true") ? 1 : 0); 349 | case DId,DUId,DInt,DUInt,DSingle,DFloat,DDate,DDateTime,DBigInt,DBigId: 350 | if( rop.match(v) ) 351 | cond += " "+rop.matched(1)+" "+m.quote(rop.matched(2)); 352 | else 353 | cond += " = "+m.quote(v); 354 | default: 355 | cond += " = "+m.quote(v); 356 | } 357 | } 358 | if( order != null ) { 359 | if( order.charAt(0) == "-" ) 360 | cond += " ORDER BY "+escape(order.substr(1))+" DESC"; 361 | else 362 | cond += " ORDER BY "+escape(order); 363 | } 364 | 365 | var sql = "SELECT * FROM " + escape(name) + " WHERE " + cond + " LIMIT " + pos + "," + count; 366 | return manager.unsafeObjects(sql, false); 367 | } 368 | 369 | static function fromTypeDescription( desc : String ) { 370 | var fdesc = desc.toUpperCase().split(" "); 371 | var ftype = fdesc.shift(); 372 | var tparam = ~/^([A-Za-z]+)\(([0-9]+)\)$/; 373 | var param = null; 374 | if( tparam.match(ftype) ) { 375 | ftype = tparam.matched(1); 376 | param = Std.parseInt(tparam.matched(2)); 377 | } 378 | var nullable = true; 379 | var t = switch( ftype ) { 380 | case "VARCHAR","CHAR": 381 | if( param == null ) 382 | null; 383 | else 384 | DString(param); 385 | case "INT": 386 | if( param == 11 && fdesc.remove("AUTO_INCREMENT") ) 387 | DId 388 | else if( param == 10 && fdesc.remove("UNSIGNED") ) { 389 | if( fdesc.remove("AUTO_INCREMENT") ) 390 | DUId 391 | else 392 | DUInt; 393 | } else if( param == 11 ) 394 | DInt; 395 | else 396 | null; 397 | case "BIGINT": 398 | if( fdesc.remove("AUTO_INCREMENT") ) DBigId else DBigInt; 399 | case "DOUBLE": DFloat; 400 | case "FLOAT": DSingle; 401 | case "DATE": DDate; 402 | case "DATETIME": DDateTime; 403 | case "TIMESTAMP": DTimeStamp; 404 | case "TINYTEXT": DTinyText; 405 | case "TEXT": DSmallText; 406 | case "MEDIUMTEXT": DText; 407 | case "BLOB": DSmallBinary; 408 | case "MEDIUMBLOB": DBinary; 409 | case "LONGBLOB": DLongBinary; 410 | case "TINYINT": 411 | switch( param ) { 412 | case 1: 413 | fdesc.remove("UNSIGNED"); 414 | DBool; 415 | case 4: 416 | DTinyInt; 417 | case 3: 418 | if( fdesc.remove("UNSIGNED") ) DTinyUInt else null; 419 | default: 420 | if( OLD_COMPAT ) 421 | DInt; 422 | else 423 | null; 424 | } 425 | case "SMALLINT": 426 | fdesc.remove("UNSIGNED") ? DSmallUInt : DSmallInt; 427 | case "MEDIUMINT": 428 | fdesc.remove("UNSIGNED") ? DMediumUInt : DMediumInt; 429 | case "BINARY": 430 | if( param == null ) 431 | null; 432 | else 433 | DBytes(param); 434 | default: 435 | null; 436 | } 437 | if( t == null ) 438 | return null; 439 | while( fdesc.length > 0 ) { 440 | var d = fdesc.shift(); 441 | switch( d ) { 442 | case "NOT": 443 | if( fdesc.shift() != "NULL" ) 444 | return null; 445 | nullable = false; 446 | case "DEFAULT": 447 | var v = fdesc.shift(); 448 | if( nullable ) { 449 | if( v == "NULL" ) 450 | continue; 451 | return null; 452 | } 453 | var def = switch( t ) { 454 | case DId, DUId, DInt, DUInt, DBool, DSingle, DFloat, DEncoded, DBigInt, DBigId, DFlags(_), DTinyInt: "'0'"; 455 | case DTinyUInt, DSmallInt, DSmallUInt, DMediumUInt, DMediumInt: "'0'"; 456 | case DTinyText, DText, DString(_), DSmallText, DSerialized: "''"; 457 | case DDateTime,DTimeStamp: 458 | if( v.length > 0 && v.charAt(v.length-1) != "'" ) 459 | v += " "+fdesc.shift(); 460 | "'0000-00-00 00:00:00'"; 461 | case DDate: "'0000-00-00'"; 462 | case DSmallBinary, DBinary, DLongBinary, DNekoSerialized, DBytes(_), DNull, DInterval: null; 463 | case DData: null; 464 | case DEnum(_): "'0'"; 465 | } 466 | if( v != def && !OLD_COMPAT ) 467 | return null; 468 | case "NULL": 469 | if( !nullable ) return null; 470 | nullable = true; 471 | continue; 472 | default: 473 | return null; 474 | } 475 | } 476 | return { t : t, nullable : nullable }; 477 | } 478 | 479 | public static function fromDescription( desc : String ) { 480 | var r = ~/^CREATE TABLE `([^`]*)` \((.*)\)( ENGINE=([^ ]+))?( AUTO_INCREMENT=[^ ]+)?( DEFAULT CHARSET=.*)?$/sm; 481 | if( !r.match(desc) ) 482 | throw "Invalid "+desc; 483 | var tname = r.matched(1); 484 | if( r.matched(4).toUpperCase() != "INNODB" ) 485 | throw "Table "+tname+" should be INNODB"; 486 | var matches = r.matched(2).split(",\n"); 487 | var field_r = ~/^[ \r\n]*`(.*)` (.*)$/; 488 | var primary_r = ~/^[ \r\n]*PRIMARY KEY +\((.*)\)[ \r\n]*$/; 489 | var index_r = ~/^[ \r\n]*(UNIQUE )?KEY `(.*)` \((.*)\)[ \r\n]*$/; 490 | var foreign_r = ~/^[ \r\n]*CONSTRAINT `(.*)` FOREIGN KEY \(`(.*)`\) REFERENCES `(.*)` \(`(.*)`\) ON DELETE (SET NULL|CASCADE)[ \r\n]*$/; 491 | var index_key_r = ~/^`?(.*?)`?(\([0-9+]\))?$/; 492 | var fields = new Map(); 493 | var nulls = new Map(); 494 | var indexes = new Map(); 495 | var relations = new Array(); 496 | var primary = null; 497 | for( f in matches ) { 498 | if( field_r.match(f) ) { 499 | var fname = field_r.matched(1); 500 | var ftype = fromTypeDescription(field_r.matched(2)); 501 | if( ftype == null ) 502 | throw "Unknown description '"+field_r.matched(2)+"'"; 503 | fields.set(fname,ftype.t); 504 | if( ftype.nullable ) 505 | nulls.set(fname,true); 506 | } else if( primary_r.match (f) ) { 507 | if( primary != null ) 508 | throw "Duplicate primary key"; 509 | primary = primary_r.matched(1).split(","); 510 | for( i in 0...primary.length ) { 511 | var k = unescape(primary[i]); 512 | primary[i] = k; 513 | } 514 | } else if( index_r.match(f) ) { 515 | var unique = index_r.matched(1); 516 | var idxname = index_r.matched(2); 517 | var fs = Lambda.list(index_r.matched(3).split(",")); 518 | indexes.set(idxname,{ keys : fs.map(function(r) { 519 | if( !index_key_r.match(r) ) throw "Invalid index key "+r; 520 | return index_key_r.matched(1); 521 | }), unique : unique != "" && unique != null, name : idxname }); 522 | } else if( foreign_r.match(f) ) { 523 | var name = foreign_r.matched(1); 524 | var key = foreign_r.matched(2); 525 | var table = foreign_r.matched(3); 526 | table = table.substr(0,1).toUpperCase() + table.substr(1); // hack for MySQL on windows 527 | var id = foreign_r.matched(4); 528 | var setnull = if( foreign_r.matched(5) == "SET NULL" ) true else null; 529 | relations.push({ name : name, key : key, table : table, id : id, setnull : setnull }); 530 | } else 531 | throw "Invalid "+f+" in "+desc; 532 | } 533 | return { 534 | table : tname, 535 | fields : fields, 536 | nulls : nulls, 537 | indexes : indexes, 538 | relations : relations, 539 | primary : primary, 540 | }; 541 | } 542 | 543 | public static function sameDBStorage( dt : TableType, rt : TableType ) { 544 | return switch( rt ) { 545 | case DEncoded: dt == DInt; 546 | case DFlags(fl, auto): auto ? (fl.length <= 8 ? dt == DTinyUInt : (fl.length <= 16 ? dt == DSmallUInt : (fl.length <= 24 ? dt == DMediumUInt : dt == DInt))) : (dt == DInt); 547 | case DSerialized: (dt == DText); 548 | case DNekoSerialized: (dt == DBinary); 549 | case DData: dt == DBinary; 550 | case DEnum(_): dt == DTinyUInt; 551 | default: false; 552 | }; 553 | } 554 | 555 | public static function allTablesRequest() { 556 | return "SHOW TABLES"; 557 | } 558 | 559 | } 560 | 561 | -------------------------------------------------------------------------------- /src/spadm/Admin.hx: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c)2012 Nicolas Cannasse 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining 5 | * a copy of this software and associated documentation files (the 6 | * "Software"), to deal in the Software without restriction, including 7 | * without limitation the rights to use, copy, modify, merge, publish, 8 | * distribute, sublicense, and/or sell copies of the Software, and to 9 | * permit persons to whom the Software is furnished to do so, subject to 10 | * the following conditions: 11 | * 12 | * The above copyright notice and this permission notice shall be 13 | * included in all copies or substantial portions of the Software. 14 | * 15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | */ 23 | package spadm; 24 | 25 | import sys.db.Object; 26 | import sys.db.Manager; 27 | import sys.db.Types; 28 | #if neko 29 | import neko.Web; 30 | import neko.Lib; 31 | #elseif php 32 | import php.Web; 33 | import php.Lib; 34 | #end 35 | import spadm.Custom; 36 | import spadm.TableInfos.TableType; 37 | import spadm.TableInfos.ManagerAccess; 38 | 39 | class Admin { 40 | 41 | var style : AdminStyle; 42 | var hasSyncAction : Bool; 43 | var countCache : Map; 44 | public var allowDrop : Bool; 45 | public var default_rights : RightsInfos; 46 | public var maxUploadSize : Int; 47 | public var maxInstanceCount : Int; 48 | 49 | public function new() { 50 | maxInstanceCount = 100; 51 | maxUploadSize = 1000000; 52 | allowDrop = false; 53 | countCache = new Map(); 54 | default_rights = { 55 | can : { 56 | insert : true, 57 | delete : true, 58 | modify : true, 59 | truncate : false, 60 | }, 61 | invisible : [], 62 | readOnly : [], 63 | }; 64 | } 65 | 66 | function execute(sql) { 67 | return Manager.cnx.request(sql); 68 | } 69 | 70 | function request( t : TableInfos, sql ) { 71 | return Manager.cnx.request(sql); 72 | } 73 | 74 | function boolResult(sql) { 75 | try { 76 | execute(sql); 77 | return true; 78 | } catch( e : Dynamic ) { 79 | return false; 80 | } 81 | } 82 | 83 | function getTables() { 84 | var tables = new Array(); 85 | var classes = Lib.getClasses(); 86 | crawl(tables,classes); 87 | tables.sort(function(t1,t2) { return if( t1.name > t2.name ) 1 else if( t1.name < t2.name ) -1 else 0; }); 88 | return tables; 89 | } 90 | 91 | function has( a : { function iterator() : Iterator; }, v : T ) { 92 | for( x in a.iterator() ) 93 | if( x == v ) 94 | return true; 95 | return false; 96 | } 97 | 98 | function crawl(tables : Array,classes : Dynamic) { 99 | for( cname in Reflect.fields(classes) ) { 100 | 101 | var v : Dynamic = Reflect.field(classes,cname); 102 | var c = cname.charAt(0); 103 | if( c >= "a" && c <= "z" ) { 104 | crawl(tables,v); 105 | continue; 106 | } 107 | if( haxe.rtti.Meta.getType(v).rtti == null ) 108 | continue; 109 | var s = Type.getSuperClass(v); 110 | while( s != null ) { 111 | if( s == Object ) { 112 | tables.push(new TableInfos(Type.getClassName(v))); 113 | break; 114 | } 115 | s = Type.getSuperClass(s); 116 | } 117 | } 118 | } 119 | 120 | public function index( ?errorMsg ) { 121 | style.begin("Tables"); 122 | style.beginForm("doSync"); 123 | style.beginTable(); 124 | var sync = false; 125 | var allTables = new List(); 126 | var rq = execute(TableInfos.allTablesRequest()); 127 | for( r in rq ) { 128 | var fieldName = Reflect.fields(r)[0]; 129 | allTables.add(Reflect.field(r,fieldName)); 130 | } 131 | var windows = Sys.systemName() == "Windows"; 132 | for( t in getTables() ) { 133 | var rights = getRights(createInstance(t)); 134 | style.beginLine(true); 135 | style.text(t.name); 136 | style.nextRow(); 137 | if( !boolResult(t.existsRequest()) ) { 138 | style.linkConfirm(t.className+"/doCreate","create"); 139 | style.text("Table is Missing !"); 140 | } else { 141 | if( needSync(t) ) 142 | sync = true; 143 | if( rights.can.insert ) 144 | style.link(t.className+"/insert","insert"); 145 | style.nextRow(); 146 | style.link(t.className+"/search","search"); 147 | if( rights.can.truncate ) 148 | style.linkConfirm(t.className+"/doCleanup","cleanup"); 149 | if( allowDrop ) { 150 | style.nextRow(); 151 | style.linkConfirm(t.className+"/doDrop","drop"); 152 | } 153 | } 154 | style.endLine(); 155 | allTables.remove(t.name); 156 | if( windows || TableInfos.OLD_COMPAT ) allTables.remove(t.name.toLowerCase()); 157 | } 158 | style.endTable(); 159 | if( sync ) 160 | style.addSubmit("Synchronize Database",true); 161 | style.endForm(); 162 | if( !allTables.isEmpty() ) { 163 | style.beginList(); 164 | for( t in allTables ) { 165 | if( t == "ForumSearch" ) 166 | continue; 167 | style.beginItem(); 168 | style.text("Table "+t+" does not have any matching object"); 169 | style.endItem(); 170 | } 171 | style.endList(); 172 | } 173 | if( errorMsg != null ) 174 | style.error(errorMsg); 175 | style.end(); 176 | } 177 | 178 | function isBinary(t) { 179 | return switch( t ) { 180 | case DBinary, DSmallBinary, DLongBinary, DBytes(_): true; 181 | default: false; 182 | }; 183 | } 184 | 185 | function canDisplay( m : ManagerAccess ) { 186 | var c = countCache.get(m.table_name); 187 | if( c != null ) 188 | return c; 189 | c = execute(TableInfos.countRequest(m,maxInstanceCount)).length < maxInstanceCount; 190 | countCache.set(m.table_name,c); 191 | return c; 192 | } 193 | 194 | function inputField( table : TableInfos, f, id : String, readonly, ?defval : Dynamic, ?rawValue : Bool ) { 195 | var prim = has(table.primary,f.name); 196 | var insert = (id == null); 197 | for( r in table.relations ) 198 | if( r.key == f.name ) { 199 | var values = null; 200 | if( canDisplay(r.manager) || r.className == "db.File" ) 201 | values = r.manager.all(false).map(function(d) { 202 | return { 203 | id : Std.string(Reflect.field(d,r.manager.table_keys[0])), 204 | str : d.toString() 205 | }; 206 | }); 207 | var cname = r.className.substr(0,3) == "db." ? r.className.substr(3) : r.className; 208 | style.choiceField( 209 | r.prop, 210 | values, 211 | Std.string(defval), 212 | cname+"/edit/", 213 | !insert && (prim || readonly), 214 | r.className == "db.File" 215 | ); 216 | return; 217 | } 218 | if( defval != null && !rawValue ) { 219 | switch( f.type ) { 220 | case DEncoded: 221 | defval = Std.parseInt(Std.string(defval)); 222 | defval = Id.decode(defval); 223 | case DSerialized: 224 | defval = new Serialized(defval).escape(); 225 | #if neko 226 | case DNekoSerialized: 227 | var v = try haxe.Serializer.run(Lib.localUnserialize(defval)) catch( e : Dynamic ) ("ERROR : " + Std.string(e)); 228 | defval = new Serialized(v).escape(); 229 | #end 230 | case DData: 231 | var str = defval.toString(); 232 | defval = new Serialized(str).escape(); 233 | default: 234 | } 235 | } 236 | if( isBinary(f.type) ) 237 | style.binField(f.name,table.nulls.exists(f.name),defval,if( insert ) null else function() { return table.name+"/doDownload/"+id+"/"+f.name; }); 238 | else if( insert && readonly ) 239 | return; 240 | else if( !insert && (prim || readonly) ) 241 | style.infoField(f.name,defval); 242 | else 243 | style.inputField(f.name,f.type,table.nulls.exists(f.name),defval); 244 | } 245 | 246 | function insert(table : TableInfos, ?params : Map, ?error : String, ?errorMsg : String ) { 247 | var binary = false; 248 | for( f in table.fields ) 249 | if( isBinary(f.type) ) { 250 | binary = true; 251 | break; 252 | } 253 | style.begin("Insert new "+table.name); 254 | style.beginForm(table.className+"/doInsert",binary,table.name); 255 | var rights = getRights(table); 256 | for( f in table.fields ) { 257 | if( f.name == error ) { 258 | style.errorField((errorMsg == null) ? "Invalid format" : errorMsg); 259 | errorMsg = null; 260 | } 261 | if( has(rights.invisible,f.name) ) 262 | continue; 263 | var readonly = has(rights.readOnly,f.name); 264 | inputField(table,f,null,readonly,if( params == null ) null else params.get(f.name), params != null ); 265 | } 266 | style.addSubmit("Insert"); 267 | style.addSubmit("Insert New",null,false,"__new"); 268 | style.endForm(); 269 | if( errorMsg != null ) 270 | style.error(errorMsg); 271 | style.end(); 272 | } 273 | 274 | function updateField( fname : String, v : String, ftype : TableType ) : Dynamic { 275 | switch( ftype ) { 276 | case DId, DUId, DBigId: 277 | return null; 278 | case DDate: 279 | var d = if( v == "NOW" || v == "NOW()" ) Date.now() else try Date.fromString(v) catch( e : Dynamic ) null; 280 | if( d == null ) 281 | return null; 282 | try d.toString() catch( e : Dynamic ) return null; 283 | return d; 284 | case DDateTime, DTimeStamp: 285 | var d = if( v == "NOW" || v == "NOW()" ) Date.now() else try Date.fromString(v) catch( e : Dynamic ) null; 286 | if( d == null ) 287 | return null; 288 | try d.toString() catch( e : Dynamic ) return null; 289 | return d; 290 | case DInt: 291 | if( v == "" ) return 0; 292 | return Std.parseInt(v); 293 | case DUInt, DFlags(_): 294 | if( v == "" ) return 0; 295 | var i = Std.parseInt(v); 296 | if( i < 0 ) 297 | return null; 298 | return i; 299 | case DTinyInt: 300 | if( v == "" ) return 0; 301 | var i = Std.parseInt(v); 302 | if( i < -128 || i > 127 ) 303 | return null; 304 | return i; 305 | case DTinyUInt: 306 | if( v == "" ) return 0; 307 | var i = Std.parseInt(v); 308 | if( i < 0 || i > 255 ) 309 | return null; 310 | return i; 311 | case DSmallInt: 312 | if( v == "" ) return 0; 313 | var i = Std.parseInt(v); 314 | if( i < -32768 || i > 32767 ) 315 | return null; 316 | return i; 317 | case DSmallUInt: 318 | if( v == "" ) return 0; 319 | var i = Std.parseInt(v); 320 | if( i < 0 || i > 65535 ) 321 | return null; 322 | return i; 323 | case DMediumInt: 324 | if( v == "" ) return 0; 325 | var i = Std.parseInt(v); 326 | if( i < -8388608 || i > 8388607 ) 327 | return null; 328 | return i; 329 | case DMediumUInt: 330 | if( v == "" ) return 0; 331 | var i = Std.parseInt(v); 332 | if( i < 0 || i > 16777215 ) 333 | return null; 334 | return i; 335 | case DBigInt: 336 | if( v == "" ) return 0; 337 | var i = Std.parseFloat(v); 338 | if( i == null || i%1 != 0 || i < -9223372036854775808.0 || i > 9223372036854775807.0 ) 339 | return null; 340 | return i; 341 | case DFloat, DSingle: 342 | if( v == "" ) return 0; 343 | var fl = Std.parseFloat(v); 344 | if( Math.isNaN(fl) ) 345 | return null; 346 | return fl; 347 | case DString(n): 348 | if( v.length > n ) 349 | return null; 350 | return v; 351 | case DTinyText: 352 | if( v.length > 255 ) 353 | return null; 354 | return v; 355 | case DSmallText, DSmallBinary: 356 | if( v.length > 0xFFFF ) 357 | return null; 358 | return v; 359 | case DText, DBinary: 360 | if( v.length > 0xFFFFFF ) 361 | return null; 362 | return v; 363 | case DBytes(n): 364 | if( v.length > n ) 365 | return null; 366 | return v; 367 | case DLongBinary: 368 | return v; 369 | case DBool: 370 | return (v == "true"); 371 | case DEncoded: 372 | if( v == "" ) return null; 373 | return try Id.encode(v) catch( e : Dynamic ) null; 374 | case DSerialized: 375 | return new Serialized(v).encode(); 376 | case DNekoSerialized: 377 | var str = new Serialized(v).encode(); 378 | var val = Lib.serialize(haxe.Unserializer.run(str)); 379 | return val; 380 | case DData: 381 | var s = new Serialized(v).encode(); 382 | if( s.length > 0xFFFFFF ) 383 | return null; 384 | return haxe.io.Bytes.ofString(s); 385 | case DEnum(e): 386 | if( v == "" ) return 0; 387 | var i = Std.parseInt(v); 388 | var ev = Type.resolveEnum(e); 389 | if( i < 0 || (ev != null && i >= Type.getEnumConstructs(ev).length) ) 390 | return null; 391 | return i; 392 | case DNull, DInterval: 393 | throw "assert"; 394 | } 395 | } 396 | 397 | function createInstance( table : TableInfos ) : Object { 398 | return Type.createEmptyInstance(table.cl); 399 | } 400 | 401 | function getRights( ?t : Object, ?table : TableInfos ) : RightsInfos { 402 | if( t == null ) 403 | t = createInstance(table); 404 | if( untyped t.dbRights == null ) 405 | return default_rights; 406 | var r : RightsInfos = untyped t.dbRights(); 407 | if( r == null ) 408 | return default_rights; 409 | if( r.can == null ) 410 | r.can = default_rights.can; 411 | return r; 412 | } 413 | 414 | function getSInfos( t : Object ) : SearchInfos { 415 | if( untyped t.dbSearch == null ) 416 | return null; 417 | return untyped t.dbSearch(); 418 | } 419 | 420 | function doInsert( table : TableInfos, params : Map ) { 421 | var inst = createInstance(table); 422 | updateParams(table,params); 423 | for( f in table.fields ) { 424 | var v = params.get(f.name); 425 | if( v == null ) { 426 | if( table.nulls.exists(f.name) ) 427 | Reflect.setField(inst,f.name,null); 428 | else 429 | for( r in table.relations ) 430 | if( f.name == r.key ) { 431 | insert(table,params,f.name); 432 | return; 433 | } 434 | continue; 435 | } 436 | var msg = null; 437 | var v = try updateField(f.name, v, f.type) catch( err : String ) { msg = err; null; }; 438 | if( v == null ) { 439 | // loop in case of error Invalid_format 440 | insert(table,params,f.name,msg); 441 | return; 442 | } 443 | Reflect.setField(inst,f.name,v); 444 | } 445 | if( table.primary.length == 1 && Reflect.field(inst,table.primary.first()) == 0 ) 446 | Reflect.deleteField(inst,table.primary.first()); 447 | try { 448 | if( !getRights(inst).can.insert ) 449 | throw "Can't insert"; 450 | inst.insert(); 451 | log("Inserted "+table.name+" "+table.identifier(inst)); 452 | } catch( e : Dynamic ) { 453 | insert(table,params,null,Std.string(e)); 454 | return; 455 | } 456 | if( params.exists("__new") ) { 457 | insert(table,params); 458 | return; 459 | } 460 | style.gotoURL(table.className+"/edit/"+table.identifier(inst)); 461 | } 462 | 463 | function doCreate(table : TableInfos) { 464 | try { 465 | execute(table.createRequest(false)); 466 | style.gotoURL(""); 467 | } catch( e : Dynamic ) { 468 | index(Std.string(e)); 469 | } 470 | } 471 | 472 | function doDrop(table : TableInfos) { 473 | if( !allowDrop ) 474 | throw "Drop not allowed"; 475 | execute(table.dropRequest()); 476 | style.gotoURL(""); 477 | } 478 | 479 | function doCleanup( table : TableInfos ) { 480 | if( !getRights(table).can.truncate ) 481 | throw "Can't cleanup"; 482 | execute(table.truncateRequest()); 483 | style.gotoURL(""); 484 | } 485 | 486 | function edit( table : TableInfos, id : String, ?params : Map, ?error : String, ?errorMsg : String ) { 487 | var obj = table.fromIdentifier(id); 488 | var objStr = try Std.string(obj) catch( e : Dynamic ) "#"+id; 489 | style.begin("Edit "+table.name+" "+objStr); 490 | if( obj == null ) { 491 | style.error("This object does not exists"); 492 | style.end(); 493 | return; 494 | } 495 | var binary = false; 496 | for( f in table.fields ) 497 | if( isBinary(f.type) ) { 498 | binary = true; 499 | break; 500 | } 501 | style.beginForm(table.className+"/doEdit/"+id,binary,table.name); 502 | var rights = getRights(obj); 503 | var hasBinary = false; 504 | for( f in table.fields ) { 505 | if( f.name == error ) { 506 | style.errorField((errorMsg == null) ? "Invalid format" : errorMsg); 507 | errorMsg = null; 508 | } 509 | if( has(rights.invisible,f.name) ) 510 | continue; 511 | var readonly = has(rights.readOnly,f.name); 512 | inputField(table,f,id,readonly,if( params == null ) Reflect.field(obj,f.name) else params.get(f.name), params != null); 513 | if( !readonly && isBinary(f.type) ) 514 | hasBinary = true; 515 | } 516 | if( rights.can.modify ) { 517 | style.addSubmit("Modify"); 518 | if( hasBinary ) 519 | style.addSubmit("Upload",null,null,"__upload"); 520 | } 521 | style.addSubmit("Cancel",table.className+"/edit/"+id); 522 | if( rights.can.delete ) 523 | style.addSubmit("Delete",table.className+"/doDelete/"+id,true); 524 | style.endForm(); 525 | if( errorMsg != null ) 526 | style.error(errorMsg); 527 | style.end(); 528 | } 529 | 530 | function doEdit( table : TableInfos, id : String, params : Map ) { 531 | var inst = table.fromIdentifier(id); 532 | if( inst == null ) { 533 | style.gotoURL(table.className+"/edit/"+id); 534 | return; 535 | } 536 | updateParams(table,params); 537 | var rights = getRights(inst); 538 | var binaries = new List(); 539 | for( f in table.fields ) { 540 | if( has(rights.readOnly,f.name) || has(rights.invisible,f.name) ) 541 | continue; 542 | var v = params.get(f.name); 543 | if( v == null ) { 544 | if( table.nulls.exists(f.name) ) 545 | Reflect.setField(inst,f.name,null); 546 | continue; 547 | } 548 | var msg = null; 549 | var v = try updateField(f.name, v, f.type) catch( err : Dynamic ) { msg = err; null; }; 550 | if( v == null ) { 551 | // insert ID into params 552 | if( table.primary != null ) { 553 | for( p in table.primary ) 554 | params.set(p,Reflect.field(inst,p)); 555 | } 556 | for( f in rights.readOnly ) 557 | params.set(f, Reflect.field(inst, f)); 558 | // error Invalid_format 559 | edit(table,id,params,f.name, msg); 560 | return; 561 | } 562 | var bin = isBinary(f.type); 563 | if( Std.is(v,String) && v == "" && bin ) 564 | continue; 565 | Reflect.setField(inst,f.name,v); 566 | if( bin ) 567 | binaries.add({ name : f.name, value : v }); 568 | } 569 | try { 570 | if( !rights.can.modify ) 571 | throw "Can't modify"; 572 | if( params.exists("__upload") ) 573 | request(table,table.updateFields(inst,binaries)); 574 | else { 575 | inst.update(); 576 | log("Updated "+table.name+" "+table.identifier(inst)); 577 | } 578 | } catch( e : Dynamic ) { 579 | edit(table,id,params,null,Std.string(e)); 580 | return; 581 | } 582 | style.gotoURL(table.className+"/edit/"+table.identifier(inst)); 583 | } 584 | 585 | function updateParams( table : TableInfos, params : Map ) { 586 | var tmp = Web.getMultipart(maxUploadSize); 587 | for( k in tmp.keys() ) 588 | params.set(k,tmp.get(k)); 589 | for( r in table.relations ) { 590 | var p = params.get(r.prop); 591 | params.remove(r.prop); 592 | if( p == null || p == "" ) 593 | continue; 594 | params.set(r.key,p); 595 | params.remove(r.prop+"__data"); 596 | params.set(r.key+"__data","on"); 597 | } 598 | for( f in table.fields ) { 599 | switch( f.type ) { 600 | case DFlags(flags,_): 601 | var vint = 0; 602 | for( i in 0...flags.length ) 603 | if( params.exists(f.name + "_" + flags[i]) ) 604 | vint |= 1 << i; 605 | if( table.nulls.exists(f.name) && !params.exists(f.name+"__data") && vint == 0 ) { 606 | params.remove(f.name); 607 | continue; 608 | } 609 | params.set(f.name, Std.string(vint)); 610 | params.set(f.name + "__data", "true"); 611 | default: 612 | } 613 | if( table.nulls.exists(f.name) && !params.exists(f.name+"__data") && (params.get(f.name) == "" || params.get(f.name) == null) ) { 614 | params.remove(f.name); 615 | continue; 616 | } 617 | if( f.type == DBool ) { 618 | var v = params.exists(f.name); 619 | params.set(f.name,if( v ) "true" else "false"); 620 | } 621 | } 622 | } 623 | 624 | function doDelete( table : TableInfos, id : String ) { 625 | var inst = table.fromIdentifier(id); 626 | if( inst == null ) { 627 | style.gotoURL(table.className+"/edit/"+id); 628 | return; 629 | } 630 | if( !getRights(inst).can.delete ) { 631 | edit(table,id,null,null,"Can't Delete"); 632 | return; 633 | } 634 | inst.delete(); 635 | log("Deleted "+table.name+" "+id); 636 | style.gotoURL(""); 637 | } 638 | 639 | function doDownload( table : TableInfos, id : String, field : String ) { 640 | var inst = table.fromIdentifier(id); 641 | if( inst == null ) { 642 | style.gotoURL(table.className+"/edit/"+id); 643 | return; 644 | } 645 | var rights = getRights(inst); 646 | var f = table.hfields.get(field); 647 | var data : String = Reflect.field(inst,field); 648 | if( has(rights.invisible,field) || data == null || !isBinary(f) ) { 649 | edit(table,id,null,null,"Can't Download data"); 650 | return; 651 | } 652 | Web.setHeader("Content-Type","text/binary"); 653 | Web.setHeader("Content-Length",Std.string(data.length)); 654 | Lib.print(data); 655 | } 656 | 657 | function search( table : TableInfos, params : Map ) { 658 | style.begin("Search "+table.name); 659 | 660 | var pagesize = 30; 661 | var page = Std.parseInt(params.get("__p")); 662 | var order = params.get("__o"); 663 | if( page == null ) 664 | page = 0; 665 | params.remove("__p"); 666 | params.remove("__o"); 667 | 668 | // save params for later usage 669 | var paramsStr = ""; 670 | for( p in params.keys() ) { 671 | var v = params.get(p); 672 | paramsStr += p+"="+StringTools.urlEncode(v)+";"; 673 | } 674 | 675 | // set nullable for all types which can be searched with empty string or no values 676 | for( f in table.fields ) 677 | switch( f.type ) { 678 | case DBool, DString(_), DTinyText, DText, DSmallText, DFlags(_): 679 | table.nulls.set(f.name,true); 680 | default: 681 | } 682 | 683 | updateParams(table,params); 684 | 685 | // remove not null fields that have not been completed 686 | for( f in table.fields ) 687 | if( !table.nulls.exists(f.name) && params.get(f.name) == "" ) 688 | params.remove(f.name); 689 | 690 | var rights = getRights(table); 691 | for( f in rights.invisible ) 692 | params.remove(f); 693 | 694 | var results = table.fromSearch(params,order,page*pagesize,pagesize+1); 695 | var hasNext = false; 696 | if( results.length > pagesize ) { 697 | results.remove(results.last()); 698 | hasNext = true; 699 | } 700 | 701 | var sinfos : SearchInfos = getSInfos(createInstance(table)); 702 | var fields; 703 | if( sinfos != null && sinfos.fields != null ) 704 | fields = sinfos.fields; 705 | else { 706 | fields = new Array(); 707 | for( f in table.fields ) { 708 | var bad = false; 709 | for( r in table.relations ) 710 | if( r.key == f.name && r.className == "db.File" ) { 711 | bad = true; 712 | break; 713 | } 714 | if( bad ) 715 | continue; 716 | fields.push(f.name); 717 | } 718 | } 719 | 720 | style.beginForm(table.className+"/search",table.name); 721 | for( f in fields ) { 722 | var t = table.hfields.get(f); 723 | if( t == null ) 724 | continue; 725 | var t = switch( t ) { 726 | case DId, DUId: DInt; 727 | case DBigId: DFloat; 728 | case DText,DSmallText: DTinyText; 729 | default: t; 730 | }; 731 | inputField(table,{ name : f, type : t },null,false,if( params == null ) null else params.get(f)); 732 | } 733 | style.addSubmit("Search"); 734 | style.endForm(); 735 | 736 | style.beginTable("results"); 737 | style.beginLine(true,"header"); 738 | style.text("actions"); 739 | 740 | if( sinfos != null && sinfos.names != null ) { 741 | for( f in sinfos.names ) { 742 | style.nextRow(true); 743 | if( table.hfields.exists(f) ) { 744 | var cur = (order == f); 745 | var curNeg = (order == "-"+f); 746 | style.link(table.className+"/search?"+paramsStr+"__o="+(cur ? "-"+f : f),(cur ? "+" : curNeg ? "-" : "") + f); 747 | } else 748 | style.text(f); 749 | } 750 | } else { 751 | for( f in table.fields ) { 752 | if( has(rights.invisible,f.name) ) 753 | continue; 754 | style.nextRow(true); 755 | var cur = (order == f.name); 756 | var curNeg = (order == "-"+f.name); 757 | style.link(table.className+"/search?"+paramsStr+"__o="+(cur ? "-"+f.name : f.name),(cur ? "+" : curNeg ? "-" : "") + f.name); 758 | } 759 | } 760 | style.endLine(); 761 | 762 | var odd = false; 763 | for( r in results ) { 764 | var k = table.fields.iterator(); 765 | style.beginLine(if( odd ) "odd" else null); 766 | style.link(table.className+"/edit/"+table.identifier(r),"Edit"); 767 | odd = !odd; 768 | if( sinfos != null && sinfos.names != null ) { 769 | var rinfos = getSInfos(r); 770 | for( v in rinfos.values ) { 771 | style.nextRow(false); 772 | style.text(Std.string(v)); 773 | } 774 | } else { 775 | var rinst = getRights(r); 776 | for( f in k ) { 777 | if( has(rights.invisible,f.name) ) 778 | continue; 779 | var data = Reflect.field(r,f.name); 780 | var str = try Std.string(data) catch( e : Dynamic ) { if(!Std.is(data,Date)) Lib.rethrow(e); "#INVALID"; }; 781 | if( str.length >= 20 ) 782 | str = str.substr(0,17) + "..."; 783 | style.nextRow(false); 784 | if( has(rinst.invisible,f.name) ) 785 | style.text("???"); 786 | else if( data == null ) 787 | style.text(str) 788 | else switch( f.type ) { 789 | case DEncoded: 790 | style.text(Id.decode(data),str); 791 | case DDate: 792 | style.text(str.substr(0,10)); // remove 00:00:00 time 793 | case DFlags(flags,_): 794 | var fl = []; 795 | for( i in 0...flags.length ) 796 | if( data & (1 << i) != 0 ) 797 | fl.push(flags[i]); 798 | str = fl.join(","); 799 | if( str.length >= 20 ) 800 | style.text(str.substr(0,17) + "...",fl.join(",")+" ("+data+")"); 801 | else 802 | style.text(str,"("+data+")"); 803 | default: 804 | style.text(str); 805 | } 806 | } 807 | } 808 | style.endLine(); 809 | } 810 | style.endTable(); 811 | 812 | if( order != null ) 813 | paramsStr += "__o="+order+";"; 814 | 815 | if( page > 0 ) 816 | style.link(table.className+"/search?"+paramsStr+"__p="+(page-1),"Previous"); 817 | else 818 | style.text("Previous"); 819 | style.text(" | "); 820 | if( hasNext ) 821 | style.link(table.className+"/search?"+paramsStr+"__p="+(page+1),"Next"); 822 | else 823 | style.text("Next"); 824 | style.end(); 825 | } 826 | 827 | function syncAction( t : TableInfos, act : Array, text : String, ?def ) { 828 | if( !hasSyncAction ) { 829 | style.beginList(); 830 | hasSyncAction = true; 831 | } 832 | style.beginItem(); 833 | style.checkBox(t.className+"@"+act.join("@"),if( def == null ) true else def); 834 | style.text(text); 835 | style.endItem(); 836 | } 837 | 838 | function doSync( params : Map ) { 839 | var order = ["create","add","reldel","idxdel","update","remove","rename","idxadd","reladd"]; 840 | var cmd = new Array(); 841 | for( p in params.keys() ) { 842 | if( !~/[A-Za-z0-9_@]*/.match(p) ) 843 | throw "Invalid command "+p; 844 | cmd.push(p.split("@")); 845 | } 846 | cmd.sort(function(c1,c2) { 847 | var p1 = 0, p2 = 0; 848 | for( i in 0...order.length ) 849 | if( order[i] == c1[1] ) 850 | p1 = i; 851 | else if( order[i] == c2[1] ) 852 | p2 = i; 853 | return p1 - p2; 854 | }); 855 | for( data in cmd ) { 856 | var tname = data.shift(); 857 | #if php 858 | // php replaces dots by _ in post keys 859 | tname = tname.split("_").join("."); 860 | #end 861 | var table = new TableInfos(tname); 862 | var act = data.shift(); 863 | var field = data.shift(); 864 | try { 865 | switch( act ) { 866 | case "create": 867 | execute(table.createRequest(false)); 868 | case "add": 869 | execute(table.addFieldRequest(field)); 870 | case "update": 871 | execute(table.updateFieldRequest(field)); 872 | case "remove": 873 | execute(table.removeFieldRequest(field)); 874 | case "rename": 875 | execute(table.renameFieldRequest(field,data.shift())); 876 | case "reladd": 877 | execute(table.addRelationRequest(field,data.shift())); 878 | case "reldel": 879 | execute(table.deleteRelationRequest(field)); 880 | case "idxadd": 881 | execute(table.addIndexRequest(data,field == "true")); 882 | case "idxdel": 883 | execute(table.deleteIndexRequest(field)); 884 | default: 885 | throw "Unknown action "+act; 886 | } 887 | } catch( e : Dynamic ) { 888 | index(Std.string(e)); 889 | return; 890 | } 891 | } 892 | style.gotoURL(""); 893 | } 894 | 895 | function indexId( i : { unique : Bool, keys : List } ) { 896 | return i.unique+"@"+i.keys.join("@"); 897 | } 898 | 899 | function needSync( t : TableInfos ) { 900 | var desc = execute(t.descriptionRequest()).getResult(1); 901 | var inf = TableInfos.fromDescription(desc); 902 | var renames = new Map(); 903 | hasSyncAction = false; 904 | // ADD/CHANGE FIELDS 905 | for( f in t.fields ) { 906 | var t2 = inf.fields.get(f.name); 907 | if( t2 == null ) { 908 | var rename = false; 909 | for( n in inf.fields.keys() ) 910 | if( !t.hfields.exists(n) && Type.enumEq(inf.fields.get(n),f.type) && inf.nulls.get(n) == t.nulls.get(f.name) ) { 911 | rename = true; 912 | renames.set(n,true); 913 | syncAction(t,["rename",n,f.name],"Rename field "+n+" to "+f.name); 914 | break; 915 | } 916 | syncAction(t,["add",f.name],"Add field "+f.name,!rename); 917 | } else { 918 | inf.fields.remove(f.name); 919 | var isnull = inf.nulls.get(f.name); 920 | var changed = false; 921 | var txt = "Change "+f.name+" : "; 922 | if( !Type.enumEq(f.type,t2) && !TableInfos.sameDBStorage(t2,f.type) ) { 923 | changed = true; 924 | txt += " S"+Std.string(t2).substr(1)+" becomes S"+Std.string(f.type).substr(1); 925 | } 926 | if( isnull != t.nulls.get(f.name) ) { 927 | if( changed ) 928 | txt += " and"; 929 | else 930 | changed = true; 931 | if( isnull ) 932 | txt += " can't be NULL"; 933 | else 934 | txt += " can be NULL"; 935 | } 936 | if( changed ) 937 | syncAction(t,["update",f.name],txt); 938 | } 939 | } 940 | // REMOVE FIELDS 941 | for( f in inf.fields.keys() ) 942 | syncAction(t,["remove",f],"Remove field "+f,!renames.exists(f)); 943 | // ADD RELATIONS 944 | for( r in t.relations ) { 945 | if( !t.isRelationActive(r) ) 946 | continue; 947 | var tname = TableInfos.unescape(r.manager.table_name); 948 | var found = false; 949 | var setnull = t.nulls.get(r.key); 950 | if( setnull && untyped r.cascade == true ) 951 | setnull = null; 952 | for( r2 in inf.relations ) 953 | if( (t.name + "_" + r.prop == r2.name || TableInfos.OLD_COMPAT) && 954 | r.key == r2.key && 955 | tname.toLowerCase() == r2.table.toLowerCase() && 956 | r.manager.table_keys.length == 1 && r.manager.table_keys[0] == r2.id && 957 | r2.setnull == setnull 958 | ) { 959 | found = true; 960 | inf.relations.remove(r2); 961 | break; 962 | } 963 | if( !found ) 964 | syncAction(t,["reladd",r.key,r.prop],"Add Relation "+r.prop+"("+r.key+") on "+tname+"("+r.manager.table_keys[0]+")"+if( setnull ) " set-null" else ""); 965 | } 966 | // REMOVE RELATIONS 967 | for( r in inf.relations ) 968 | syncAction(t,["reldel",r.name],"Remove Relation "+r.name+"("+r.key+") on "+r.table+"("+r.id+")"+if( r.setnull ) " set-null" else ""); 969 | // INDEXES 970 | var hidx = new Map(); 971 | for( i in t.indexes ) 972 | hidx.set(indexId(i),i); 973 | var used = new List(); 974 | for( r in t.relations ) { 975 | var found : { keys : List, unique : Bool } = null; 976 | for( i in t.indexes ) 977 | if( i.keys.first() == r.key && (found == null || found.keys.length < i.keys.length) ) 978 | found = i; 979 | if( found == null ) { 980 | // in primary key ? 981 | if( t.primary.first() == r.key ) 982 | continue; 983 | // default relation-index 984 | found = { keys : Lambda.list([r.key]), unique : false }; 985 | } 986 | hidx.remove(indexId(found)); 987 | for( i in inf.indexes ) 988 | if( i.keys.join("#") == found.keys.join("#") && i.unique == found.unique ) { 989 | used.add(i); 990 | found = null; 991 | break; 992 | } 993 | // we need it 994 | if( found != null ) 995 | hidx.set(indexId(found),found); 996 | } 997 | for( i in used ) 998 | inf.indexes.remove(i.name); 999 | for( iname in inf.indexes.keys() ) { 1000 | var i = inf.indexes.get(iname); 1001 | if( !hidx.remove(indexId(i)) ) 1002 | syncAction(t,["idxdel",iname],"Remove "+(if( i.unique ) "Unique " else "")+"Index "+iname+" ("+i.keys.join(",")+")"); 1003 | } 1004 | for( i in hidx ) 1005 | syncAction(t,["idxadd",indexId(i)],"Add "+(if( i.unique ) "Unique " else "")+"Index ("+i.keys.join(",")+")"); 1006 | // PRIMARY KEYS 1007 | if( (inf.primary == null) != (t.primary == null) || (inf.primary != null && inf.primary.join("-") != t.primary.join("-")) ) { 1008 | style.text("PRIMARY KEY CHANGED !"); 1009 | hasSyncAction = true; 1010 | } 1011 | if( hasSyncAction ) 1012 | style.endList(); 1013 | return hasSyncAction; 1014 | } 1015 | 1016 | public function process( ?url : Array, ?baseUrl = "/db/" ) { 1017 | if( url == null ) { 1018 | url = Web.getURI().substr(baseUrl.length).split("/"); 1019 | } 1020 | if( url.length == 0 ) url.push(""); 1021 | var params = Web.getParams(); 1022 | switch( url[0] ) { 1023 | case "": 1024 | style = new AdminStyle(null); 1025 | index(); 1026 | return; 1027 | case "doSync": 1028 | style = new AdminStyle(null); 1029 | doSync(params); 1030 | return; 1031 | } 1032 | var table = new TableInfos(url.shift()); 1033 | style = new AdminStyle(table); 1034 | var act = url.shift(); 1035 | switch( act ) { 1036 | case "insert": 1037 | insert(table,params); 1038 | case "doInsert": 1039 | doInsert(table,params); 1040 | case "edit": 1041 | edit(table,url.join("/")); 1042 | case "doEdit": 1043 | doEdit(table,url.join("/"),params); 1044 | case "doCreate": 1045 | doCreate(table); 1046 | case "doDrop": 1047 | doDrop(table); 1048 | case "doCleanup": 1049 | doCleanup(table); 1050 | case "doDelete": 1051 | doDelete(table,url.join("/")); 1052 | case "doDownload": 1053 | doDownload(table,url.shift(),url.shift()); 1054 | case "search": 1055 | search(table,params); 1056 | default: 1057 | throw "Unknown action "+act; 1058 | } 1059 | } 1060 | 1061 | static function log(msg:String) { 1062 | #if neko 1063 | Web.logMessage("[DBADM] "+Web.getHostName()+" "+Date.now().toString()+" "+Web.getClientIP()+" - "+msg); 1064 | #end 1065 | } 1066 | 1067 | public static function handler( ?baseUrl:String ) { 1068 | Manager.initialize(); // make sure it's been done 1069 | try { 1070 | new Admin().process(baseUrl); 1071 | } catch( e : Dynamic ) { 1072 | // rollback in case of multiple delete/update - no effect on DB struct changes 1073 | // since they are done outside of transaction 1074 | Lib.print("
    ");
    1075 | 			Lib.print(Std.string(e));
    1076 | 			Lib.print(haxe.CallStack.toString(haxe.CallStack.exceptionStack()));
    1077 | 			Lib.print("
    "); 1078 | Manager.cnx.rollback(); 1079 | } 1080 | } 1081 | 1082 | public static function initializeDatabase( initIndexes = true, initRelations = true ) { 1083 | var a = new Admin(); 1084 | var tables = a.getTables(); 1085 | for( t in tables ) 1086 | a.execute(t.createRequest(false)); 1087 | for( t in tables ) { 1088 | if( initIndexes ) 1089 | for( i in t.indexes ) 1090 | a.execute(t.addIndexRequest(Lambda.array(i.keys), i.unique)); 1091 | if( initRelations ) 1092 | for( r in t.relations ) 1093 | a.execute(t.addRelationRequest(r.key, r.prop)); 1094 | } 1095 | } 1096 | 1097 | } 1098 | --------------------------------------------------------------------------------