From 723dc3afba0f07ec4f66c3244bcad6dd3b318170 Mon Sep 17 00:00:00 2001 From: Parziphal Date: Wed, 2 Oct 2013 11:14:53 -0500 Subject: [PATCH] directory restructured --- composer.json | 7 +- .../ActionController/ActionController.php | 85 + lib/Rails/ActionController/Base.php | 808 ++ lib/Rails/ActionController/Cookie.php | 96 + lib/Rails/ActionController/Cookies.php | 94 + .../Exception/BadMethodCallException.php | 6 + .../Exception/BodyAlreadySetException.php | 6 + .../Exception/DoubleRenderException.php | 7 + .../Exception/ExceptionInterface.php | 6 + .../Exception/InvalidArgumentException.php | 6 + .../Exception/RuntimeException.php | 6 + .../Exception/UnknownActionException.php | 11 + .../ActionController/ExceptionHandler.php | 50 + lib/Rails/ActionController/Response.php | 81 + lib/Rails/ActionController/Response/Base.php | 27 + lib/Rails/ActionController/Response/Error.php | 94 + .../Exception/ActionNotFoundException.php | 6 + .../Response/Exception/ExceptionInterface.php | 6 + .../Exception/ViewNotFoundException.php | 7 + lib/Rails/ActionController/Response/File.php | 28 + .../ActionController/Response/Inline.php | 26 + lib/Rails/ActionController/Response/Json.php | 55 + .../ActionController/Response/Lambda.php | 25 + .../ActionController/Response/Partial.php | 29 + .../ActionController/Response/Redirect.php | 27 + .../ActionController/Response/Template.php | 85 + lib/Rails/ActionController/Response/View.php | 64 + lib/Rails/ActionController/Response/Xml.php | 29 + lib/Rails/ActionDispatch/ActionDispatch.php | 143 + .../Exception/ExceptionInterface.php | 6 + .../Exception/LogicException.php | 6 + .../Exception/RuntimeException.php | 6 + .../Http/Exception/ExceptionInterface.php | 6 + .../Exception/InvalidArgumentException.php | 6 + .../Http/Exception/LogicException.php | 6 + lib/Rails/ActionDispatch/Http/Headers.php | 166 + lib/Rails/ActionDispatch/Http/Parameters.php | 314 + lib/Rails/ActionDispatch/Http/Session.php | 64 + .../ActionDispatch/Http/UploadedFile.php | 59 + lib/Rails/ActionDispatch/Request.php | 175 + lib/Rails/ActionMailer/ActionMailer.php | 147 + lib/Rails/ActionMailer/Base.php | 135 + lib/Rails/ActionMailer/Deliverer.php | 230 + .../Exception/ExceptionInterface.php | 6 + .../Exception/InvalidArgumentException.php | 6 + .../Exception/RuntimeException.php | 6 + lib/Rails/ActionMailer/Template.php | 32 + lib/Rails/ActionView/ActionView.php | 88 + lib/Rails/ActionView/Base.php | 204 + .../Exception/BadMethodCallException.php | 6 + .../Exception/ExceptionInterface.php | 6 + .../Exception/InvalidArgumentException.php | 6 + .../ActionView/Exception/RuntimeException.php | 6 + lib/Rails/ActionView/FormBuilder.php | 61 + lib/Rails/ActionView/Helper.php | 123 + lib/Rails/ActionView/Helper/Base.php | 17 + .../ActionView/Helper/Methods/Assets.php | 30 + lib/Rails/ActionView/Helper/Methods/Date.php | 62 + lib/Rails/ActionView/Helper/Methods/Form.php | 224 + .../ActionView/Helper/Methods/FormTag.php | 219 + .../ActionView/Helper/Methods/Header.php | 95 + lib/Rails/ActionView/Helper/Methods/Html.php | 114 + .../ActionView/Helper/Methods/Inflections.php | 55 + .../ActionView/Helper/Methods/JavaScript.php | 91 + .../ActionView/Helper/Methods/Number.php | 26 + lib/Rails/ActionView/Helper/Methods/Tag.php | 62 + lib/Rails/ActionView/Helper/Methods/Text.php | 33 + .../WillPaginate.delete/AbstractRenderer.php | 168 + .../WillPaginate.delete/BootstrapRenderer.php | 52 + .../WillPaginate.delete/LegacyRenderer.php | 8 + lib/Rails/ActionView/Layout.php | 31 + lib/Rails/ActionView/Partial.php | 17 + lib/Rails/ActionView/Template.php | 288 + .../Template/Exception/ExceptionInterface.php | 6 + .../Exception/LayoutMissingException.php | 7 + .../Exception/OutputLeakedException.php | 6 + .../Exception/TemplateMissingException.php | 6 + lib/Rails/ActionView/ViewHelpers.php | 109 + lib/Rails/ActionView/Xml.php | 69 + lib/Rails/ActiveModel/Collection.php | 366 + lib/Rails/ActiveModel/Errors.php | 107 + .../Exception/ExceptionInterface.php | 6 + .../Exception/InvalidArgumentException.php | 6 + lib/Rails/ActiveRecord/ActiveRecord.php | 174 + .../Adapter/AbstractQueryBuilder.php | 62 + .../ActiveRecord/Adapter/AbstractTable.php | 12 + .../Exception/BadMethodCallException.php | 6 + .../Adapter/Exception/ExceptionInterface.php | 6 + .../Adapter/Exception/RuntimeException.php | 6 + .../Adapter/MySql/QueryBuilder.php | 101 + .../ActiveRecord/Adapter/MySql/Table.php | 55 + .../Adapter/Sqlite/QueryBuilder.php | 99 + .../ActiveRecord/Adapter/Sqlite/Table.php | 36 + lib/Rails/ActiveRecord/Base.php | 1029 +++ .../Base/Methods/AssociationMethods.php | 1 + .../Base/Methods/AttributeMethods.php | 265 + .../Base/Methods/CounterMethods.php | 26 + .../Base/Methods/ModelSchemaMethods.php | 51 + .../Base/Methods/RelationMethods.php | 240 + .../Base/Methods/ScopingMethods.php | 63 + lib/Rails/ActiveRecord/Collection.php | 73 + lib/Rails/ActiveRecord/Connection.php | 265 + .../Exception/BadMethodCallException.php | 6 + .../Exception/ExceptionInterface.php | 6 + .../Exception/InvalidArgumentException.php | 6 + .../ActiveRecord/Exception/LogicException.php | 6 + .../Exception/QueryAwareTrait.php | 27 + .../ActiveRecord/Exception/QueryException.php | 7 + .../Exception/RecordNotFoundException.php | 9 + .../Exception/RuntimeException.php | 6 + lib/Rails/ActiveRecord/ModelSchema.php | 150 + lib/Rails/ActiveRecord/Registry.php | 146 + lib/Rails/ActiveRecord/Relation.php | 6 + .../Relation/AbstractRelation.php | 577 ++ .../ActiveRecord/Relation/Association.php | 48 + .../Exception/ExceptionInterface.php | 6 + .../Validation/Exception/RuntimeException.php | 6 + .../ActiveRecord/Validation/Validator.php | 249 + .../Inflector/DefaultEnglishInflections.php | 82 + .../ActiveSupport/Inflector/Inflections.php | 132 + .../ActiveSupport/Inflector/Inflector.php | 201 + lib/Rails/ActiveSupport/Inflector/Word.php | 82 + lib/Rails/Application/Base.php | 356 + .../Exception/ExceptionInterface.php | 6 + .../Exception/RuntimeException.php | 6 + lib/Rails/ArrayHelper/Base.php | 50 + .../Exception/ExceptionInterface.php | 6 + .../Exception/InvalidArgumentException.php | 6 + lib/Rails/ArrayHelper/GlobalVar.php | 31 + lib/Rails/Assets/Assets.php | 522 ++ lib/Rails/Assets/Compiler.php | 9 + .../Assets/Exception/ExceptionInterface.php | 6 + .../Exception/FileNotFoundException.php | 6 + .../Exception/InvalidArgumentException.php | 6 + .../Assets/Exception/RuntimeException.php | 6 + lib/Rails/Assets/File.php | 226 + lib/Rails/Assets/Initializer.php | 15 + lib/Rails/Assets/Parser/Base.php | 406 + .../Javascript/ClosureApi/ClosureApi.php | 87 + .../Exception/BlankResponseException.php | 6 + .../Exception/ErrorsOnCodeException.php | 6 + .../Exception/ExceptionInterface.php | 6 + lib/Rails/Assets/Parser/PHParser - Copy.php | 72 + lib/Rails/Assets/Parser/PHParser.php | 26 + lib/Rails/Assets/Server.php | 153 + lib/Rails/Assets/Traits/AssetPathTrait.php | 31 + lib/Rails/Cache/Cache.php | 92 + lib/Rails/Cache/Entry.php | 171 + .../Cache/Exception/ExceptionInterface.php | 6 + .../Cache/Exception/RuntimeException.php | 6 + lib/Rails/Cache/Store/AbstractStore.php | 15 + lib/Rails/Cache/Store/FileStore.php | 61 + lib/Rails/Cache/Store/FileStore/Entry.php | 181 + lib/Rails/Cache/Store/MemCachedStore.php | 71 + lib/Rails/Config/Config.php | 88 + lib/Rails/Config/default_config.php | 295 + lib/Rails/Console/ApplicationConsole.php | 145 + lib/Rails/Console/Console.php | 16 + lib/Rails/Console/Generators/Generator.php | 99 + lib/Rails/Console/Generators/Model.php | 7 + .../Exception/BadFunctionCallException.php | 7 + .../Exception/BadMethodCallException.php | 7 + lib/Rails/Exception/DomainException.php | 7 + lib/Rails/Exception/ErrorException.php | 7 + lib/Rails/Exception/Exception.php | 7 + lib/Rails/Exception/ExceptionInterface.php | 6 + lib/Rails/Exception/ExceptionTrait.php | 61 + .../Exception/InvalidArgumentException.php | 7 + lib/Rails/Exception/LengthException.php | 7 + lib/Rails/Exception/LogicException.php | 7 + lib/Rails/Exception/OutOfBoundsException.php | 7 + lib/Rails/Exception/OutOfRangeException.php | 7 + lib/Rails/Exception/OverflowException.php | 7 + lib/Rails/Exception/PHPError/Base.php | 17 + lib/Rails/Exception/PHPError/Catchable.php | 7 + .../Exception/PHPError/ExceptionInterface.php | 6 + lib/Rails/Exception/PHPError/Notice.php | 7 + lib/Rails/Exception/PHPError/Unknown.php | 7 + lib/Rails/Exception/PHPError/UserNotice.php | 7 + lib/Rails/Exception/PHPError/UserWarning.php | 7 + lib/Rails/Exception/PHPError/Warning.php | 7 + lib/Rails/Exception/RangeException.php | 7 + lib/Rails/Exception/Reporting/Reporter.php | 194 + lib/Rails/Exception/RuntimeException.php | 7 + lib/Rails/Exception/UnderflowException.php | 7 + .../Exception/UnexpectedValueException.php | 7 + .../I18n/Exception/ExceptionInterface.php | 6 + .../Exception/InvalidArgumentException.php | 6 + lib/Rails/I18n/Exception/RuntimeException.php | 6 + lib/Rails/I18n/I18n.php | 222 + lib/Rails/I18n/Locales/AbstractLocales.php | 12 + lib/Rails/I18n/Locales/En.php | 54 + lib/Rails/I18n/Locales/Es.php | 54 + .../Exception/ClassNotFoundException.php | 6 + .../Loader/Exception/ExceptionInterface.php | 6 + .../Exception/FileNotFoundException.php | 6 + .../Loader/Exception/RuntimeException.php | 6 + lib/Rails/Loader/Loader.php | 121 + lib/Rails/Log/Formatter/Simple.php | 43 + lib/Rails/Log/Logger.php | 116 + lib/Rails/Panel/assets/panel.css | 6 + .../Panel/controllers/AdminController.php | 140 + .../controllers/ApplicationController.php | 19 + lib/Rails/Panel/helpers/ApplicationHelper.php | 14 + lib/Rails/Panel/traits/AdminController.php | 6 + .../Panel/traits/ApplicationController.php | 6 + .../Panel/views/admin/compile_assets.php | 33 + lib/Rails/Panel/views/admin/create_files.php | 33 + lib/Rails/Panel/views/admin/forbidden.php | 1 + lib/Rails/Panel/views/admin/index.php | 1 + lib/Rails/Panel/views/admin/show_routes.php | 20 + lib/Rails/Panel/views/layouts/application.php | 43 + lib/Rails/Paths/Path.php | 83 + lib/Rails/Paths/Paths.php | 15 + lib/Rails/Rails.php | 520 ++ .../Exception/BadMethodCallException.php | 6 + .../Routing/Exception/ExceptionInterface.php | 6 + .../Exception/InvalidArgumentException.php | 6 + .../Routing/Exception/NotFoundException.php | 11 + .../Exception/RoutingErrorException.php | 11 + .../Routing/Exception/RuntimeException.php | 6 + lib/Rails/Routing/Mapper.php | 583 ++ lib/Rails/Routing/Route/HiddenRoute.php | 6 + lib/Rails/Routing/Route/PanelRoute.php | 6 + lib/Rails/Routing/Route/Route.php | 808 ++ lib/Rails/Routing/Route/RouteSet.php | 94 + lib/Rails/Routing/Router.php | 248 + .../Routing/Traits/NamedPathAwareTrait.php | 24 + lib/Rails/Routing/UrlFor.php | 198 + .../Exception/ExceptionInterface.php | 6 + .../UrlHelpers/Exception/RuntimeException.php | 7 + lib/Rails/Routing/UrlHelpers/UrlHelpers.php | 125 + lib/Rails/Routing/UrlToken.php | 89 + .../Exception/ExceptionInterface.php | 6 + .../Exception/RuntimeException.php | 6 + .../ServiceManager/ServiceAwareTrait.php | 10 + lib/Rails/ServiceManager/ServiceManager.php | 45 + lib/Rails/SystemExit/SystemExit.php | 32 + lib/Rails/Toolbox/ArrayTools.php | 34 + lib/Rails/Toolbox/ClassTools.php | 25 + lib/Rails/Toolbox/DbTools.php | 53 + .../FileGenerators/AbstractGenerator.php | 7 + .../FileGenerators/ControllerGenerator.php | 101 + .../Exception/ExceptionInterface.php | 6 + .../Exception/FileExistsException.php | 6 + .../Exception/FileNotCreatedException.php | 6 + .../FileGenerators/GeneratorInterface.php | 6 + .../Toolbox/FileGenerators/ModelGenerator.php | 101 + lib/Rails/Toolbox/FileTools.php | 88 + lib/Rails/Toolbox/functions.php | 25 + lib/Rails/Traits/ArrayObjectAccess.php | 69 + .../Exception/ExceptionInterface.php | 6 + .../Exception/InvalidArgumentException.php | 6 + .../Validation/Exception/RuntimeException.php | 6 + lib/Rails/Validation/Validation.php | 151 + .../Xml/Exception/ExceptionInterface.php | 6 + .../Exception/InvalidArgumentException.php | 6 + lib/Rails/Xml/Xml.php | 77 + lib/Rails/Yaml/Parser.php | 59 + vendor/README | 1 + vendor/assets/stylesheets/bootstrap.css | 6805 +++++++++++++++++ 261 files changed, 25726 insertions(+), 5 deletions(-) create mode 100755 lib/Rails/ActionController/ActionController.php create mode 100755 lib/Rails/ActionController/Base.php create mode 100755 lib/Rails/ActionController/Cookie.php create mode 100755 lib/Rails/ActionController/Cookies.php create mode 100755 lib/Rails/ActionController/Exception/BadMethodCallException.php create mode 100755 lib/Rails/ActionController/Exception/BodyAlreadySetException.php create mode 100755 lib/Rails/ActionController/Exception/DoubleRenderException.php create mode 100755 lib/Rails/ActionController/Exception/ExceptionInterface.php create mode 100755 lib/Rails/ActionController/Exception/InvalidArgumentException.php create mode 100755 lib/Rails/ActionController/Exception/RuntimeException.php create mode 100755 lib/Rails/ActionController/Exception/UnknownActionException.php create mode 100755 lib/Rails/ActionController/ExceptionHandler.php create mode 100755 lib/Rails/ActionController/Response.php create mode 100755 lib/Rails/ActionController/Response/Base.php create mode 100755 lib/Rails/ActionController/Response/Error.php create mode 100755 lib/Rails/ActionController/Response/Exception/ActionNotFoundException.php create mode 100755 lib/Rails/ActionController/Response/Exception/ExceptionInterface.php create mode 100755 lib/Rails/ActionController/Response/Exception/ViewNotFoundException.php create mode 100755 lib/Rails/ActionController/Response/File.php create mode 100755 lib/Rails/ActionController/Response/Inline.php create mode 100755 lib/Rails/ActionController/Response/Json.php create mode 100755 lib/Rails/ActionController/Response/Lambda.php create mode 100755 lib/Rails/ActionController/Response/Partial.php create mode 100755 lib/Rails/ActionController/Response/Redirect.php create mode 100755 lib/Rails/ActionController/Response/Template.php create mode 100755 lib/Rails/ActionController/Response/View.php create mode 100755 lib/Rails/ActionController/Response/Xml.php create mode 100755 lib/Rails/ActionDispatch/ActionDispatch.php create mode 100755 lib/Rails/ActionDispatch/Exception/ExceptionInterface.php create mode 100755 lib/Rails/ActionDispatch/Exception/LogicException.php create mode 100755 lib/Rails/ActionDispatch/Exception/RuntimeException.php create mode 100755 lib/Rails/ActionDispatch/Http/Exception/ExceptionInterface.php create mode 100755 lib/Rails/ActionDispatch/Http/Exception/InvalidArgumentException.php create mode 100755 lib/Rails/ActionDispatch/Http/Exception/LogicException.php create mode 100755 lib/Rails/ActionDispatch/Http/Headers.php create mode 100755 lib/Rails/ActionDispatch/Http/Parameters.php create mode 100755 lib/Rails/ActionDispatch/Http/Session.php create mode 100755 lib/Rails/ActionDispatch/Http/UploadedFile.php create mode 100755 lib/Rails/ActionDispatch/Request.php create mode 100755 lib/Rails/ActionMailer/ActionMailer.php create mode 100755 lib/Rails/ActionMailer/Base.php create mode 100755 lib/Rails/ActionMailer/Deliverer.php create mode 100755 lib/Rails/ActionMailer/Exception/ExceptionInterface.php create mode 100755 lib/Rails/ActionMailer/Exception/InvalidArgumentException.php create mode 100755 lib/Rails/ActionMailer/Exception/RuntimeException.php create mode 100755 lib/Rails/ActionMailer/Template.php create mode 100755 lib/Rails/ActionView/ActionView.php create mode 100755 lib/Rails/ActionView/Base.php create mode 100755 lib/Rails/ActionView/Exception/BadMethodCallException.php create mode 100755 lib/Rails/ActionView/Exception/ExceptionInterface.php create mode 100755 lib/Rails/ActionView/Exception/InvalidArgumentException.php create mode 100755 lib/Rails/ActionView/Exception/RuntimeException.php create mode 100755 lib/Rails/ActionView/FormBuilder.php create mode 100755 lib/Rails/ActionView/Helper.php create mode 100755 lib/Rails/ActionView/Helper/Base.php create mode 100755 lib/Rails/ActionView/Helper/Methods/Assets.php create mode 100755 lib/Rails/ActionView/Helper/Methods/Date.php create mode 100755 lib/Rails/ActionView/Helper/Methods/Form.php create mode 100755 lib/Rails/ActionView/Helper/Methods/FormTag.php create mode 100755 lib/Rails/ActionView/Helper/Methods/Header.php create mode 100755 lib/Rails/ActionView/Helper/Methods/Html.php create mode 100755 lib/Rails/ActionView/Helper/Methods/Inflections.php create mode 100755 lib/Rails/ActionView/Helper/Methods/JavaScript.php create mode 100755 lib/Rails/ActionView/Helper/Methods/Number.php create mode 100755 lib/Rails/ActionView/Helper/Methods/Tag.php create mode 100755 lib/Rails/ActionView/Helper/Methods/Text.php create mode 100755 lib/Rails/ActionView/Helper/WillPaginate.delete/AbstractRenderer.php create mode 100755 lib/Rails/ActionView/Helper/WillPaginate.delete/BootstrapRenderer.php create mode 100755 lib/Rails/ActionView/Helper/WillPaginate.delete/LegacyRenderer.php create mode 100755 lib/Rails/ActionView/Layout.php create mode 100755 lib/Rails/ActionView/Partial.php create mode 100755 lib/Rails/ActionView/Template.php create mode 100755 lib/Rails/ActionView/Template/Exception/ExceptionInterface.php create mode 100755 lib/Rails/ActionView/Template/Exception/LayoutMissingException.php create mode 100755 lib/Rails/ActionView/Template/Exception/OutputLeakedException.php create mode 100755 lib/Rails/ActionView/Template/Exception/TemplateMissingException.php create mode 100755 lib/Rails/ActionView/ViewHelpers.php create mode 100755 lib/Rails/ActionView/Xml.php create mode 100755 lib/Rails/ActiveModel/Collection.php create mode 100755 lib/Rails/ActiveModel/Errors.php create mode 100755 lib/Rails/ActiveModel/Exception/ExceptionInterface.php create mode 100755 lib/Rails/ActiveModel/Exception/InvalidArgumentException.php create mode 100755 lib/Rails/ActiveRecord/ActiveRecord.php create mode 100755 lib/Rails/ActiveRecord/Adapter/AbstractQueryBuilder.php create mode 100755 lib/Rails/ActiveRecord/Adapter/AbstractTable.php create mode 100755 lib/Rails/ActiveRecord/Adapter/Exception/BadMethodCallException.php create mode 100755 lib/Rails/ActiveRecord/Adapter/Exception/ExceptionInterface.php create mode 100755 lib/Rails/ActiveRecord/Adapter/Exception/RuntimeException.php create mode 100755 lib/Rails/ActiveRecord/Adapter/MySql/QueryBuilder.php create mode 100755 lib/Rails/ActiveRecord/Adapter/MySql/Table.php create mode 100755 lib/Rails/ActiveRecord/Adapter/Sqlite/QueryBuilder.php create mode 100755 lib/Rails/ActiveRecord/Adapter/Sqlite/Table.php create mode 100755 lib/Rails/ActiveRecord/Base.php create mode 100755 lib/Rails/ActiveRecord/Base/Methods/AssociationMethods.php create mode 100755 lib/Rails/ActiveRecord/Base/Methods/AttributeMethods.php create mode 100755 lib/Rails/ActiveRecord/Base/Methods/CounterMethods.php create mode 100755 lib/Rails/ActiveRecord/Base/Methods/ModelSchemaMethods.php create mode 100755 lib/Rails/ActiveRecord/Base/Methods/RelationMethods.php create mode 100755 lib/Rails/ActiveRecord/Base/Methods/ScopingMethods.php create mode 100755 lib/Rails/ActiveRecord/Collection.php create mode 100755 lib/Rails/ActiveRecord/Connection.php create mode 100755 lib/Rails/ActiveRecord/Exception/BadMethodCallException.php create mode 100755 lib/Rails/ActiveRecord/Exception/ExceptionInterface.php create mode 100755 lib/Rails/ActiveRecord/Exception/InvalidArgumentException.php create mode 100755 lib/Rails/ActiveRecord/Exception/LogicException.php create mode 100755 lib/Rails/ActiveRecord/Exception/QueryAwareTrait.php create mode 100755 lib/Rails/ActiveRecord/Exception/QueryException.php create mode 100755 lib/Rails/ActiveRecord/Exception/RecordNotFoundException.php create mode 100755 lib/Rails/ActiveRecord/Exception/RuntimeException.php create mode 100755 lib/Rails/ActiveRecord/ModelSchema.php create mode 100755 lib/Rails/ActiveRecord/Registry.php create mode 100755 lib/Rails/ActiveRecord/Relation.php create mode 100755 lib/Rails/ActiveRecord/Relation/AbstractRelation.php create mode 100755 lib/Rails/ActiveRecord/Relation/Association.php create mode 100755 lib/Rails/ActiveRecord/Validation/Exception/ExceptionInterface.php create mode 100755 lib/Rails/ActiveRecord/Validation/Exception/RuntimeException.php create mode 100755 lib/Rails/ActiveRecord/Validation/Validator.php create mode 100755 lib/Rails/ActiveSupport/Inflector/DefaultEnglishInflections.php create mode 100755 lib/Rails/ActiveSupport/Inflector/Inflections.php create mode 100755 lib/Rails/ActiveSupport/Inflector/Inflector.php create mode 100755 lib/Rails/ActiveSupport/Inflector/Word.php create mode 100755 lib/Rails/Application/Base.php create mode 100755 lib/Rails/Application/Exception/ExceptionInterface.php create mode 100755 lib/Rails/Application/Exception/RuntimeException.php create mode 100755 lib/Rails/ArrayHelper/Base.php create mode 100755 lib/Rails/ArrayHelper/Exception/ExceptionInterface.php create mode 100755 lib/Rails/ArrayHelper/Exception/InvalidArgumentException.php create mode 100755 lib/Rails/ArrayHelper/GlobalVar.php create mode 100755 lib/Rails/Assets/Assets.php create mode 100755 lib/Rails/Assets/Compiler.php create mode 100755 lib/Rails/Assets/Exception/ExceptionInterface.php create mode 100755 lib/Rails/Assets/Exception/FileNotFoundException.php create mode 100755 lib/Rails/Assets/Exception/InvalidArgumentException.php create mode 100755 lib/Rails/Assets/Exception/RuntimeException.php create mode 100755 lib/Rails/Assets/File.php create mode 100755 lib/Rails/Assets/Initializer.php create mode 100755 lib/Rails/Assets/Parser/Base.php create mode 100755 lib/Rails/Assets/Parser/Javascript/ClosureApi/ClosureApi.php create mode 100755 lib/Rails/Assets/Parser/Javascript/ClosureApi/Exception/BlankResponseException.php create mode 100755 lib/Rails/Assets/Parser/Javascript/ClosureApi/Exception/ErrorsOnCodeException.php create mode 100755 lib/Rails/Assets/Parser/Javascript/ClosureApi/Exception/ExceptionInterface.php create mode 100755 lib/Rails/Assets/Parser/PHParser - Copy.php create mode 100755 lib/Rails/Assets/Parser/PHParser.php create mode 100755 lib/Rails/Assets/Server.php create mode 100755 lib/Rails/Assets/Traits/AssetPathTrait.php create mode 100755 lib/Rails/Cache/Cache.php create mode 100755 lib/Rails/Cache/Entry.php create mode 100755 lib/Rails/Cache/Exception/ExceptionInterface.php create mode 100755 lib/Rails/Cache/Exception/RuntimeException.php create mode 100755 lib/Rails/Cache/Store/AbstractStore.php create mode 100755 lib/Rails/Cache/Store/FileStore.php create mode 100755 lib/Rails/Cache/Store/FileStore/Entry.php create mode 100755 lib/Rails/Cache/Store/MemCachedStore.php create mode 100755 lib/Rails/Config/Config.php create mode 100755 lib/Rails/Config/default_config.php create mode 100755 lib/Rails/Console/ApplicationConsole.php create mode 100755 lib/Rails/Console/Console.php create mode 100755 lib/Rails/Console/Generators/Generator.php create mode 100755 lib/Rails/Console/Generators/Model.php create mode 100755 lib/Rails/Exception/BadFunctionCallException.php create mode 100755 lib/Rails/Exception/BadMethodCallException.php create mode 100755 lib/Rails/Exception/DomainException.php create mode 100755 lib/Rails/Exception/ErrorException.php create mode 100755 lib/Rails/Exception/Exception.php create mode 100755 lib/Rails/Exception/ExceptionInterface.php create mode 100755 lib/Rails/Exception/ExceptionTrait.php create mode 100755 lib/Rails/Exception/InvalidArgumentException.php create mode 100755 lib/Rails/Exception/LengthException.php create mode 100755 lib/Rails/Exception/LogicException.php create mode 100755 lib/Rails/Exception/OutOfBoundsException.php create mode 100755 lib/Rails/Exception/OutOfRangeException.php create mode 100755 lib/Rails/Exception/OverflowException.php create mode 100755 lib/Rails/Exception/PHPError/Base.php create mode 100755 lib/Rails/Exception/PHPError/Catchable.php create mode 100755 lib/Rails/Exception/PHPError/ExceptionInterface.php create mode 100755 lib/Rails/Exception/PHPError/Notice.php create mode 100755 lib/Rails/Exception/PHPError/Unknown.php create mode 100755 lib/Rails/Exception/PHPError/UserNotice.php create mode 100755 lib/Rails/Exception/PHPError/UserWarning.php create mode 100755 lib/Rails/Exception/PHPError/Warning.php create mode 100755 lib/Rails/Exception/RangeException.php create mode 100755 lib/Rails/Exception/Reporting/Reporter.php create mode 100755 lib/Rails/Exception/RuntimeException.php create mode 100755 lib/Rails/Exception/UnderflowException.php create mode 100755 lib/Rails/Exception/UnexpectedValueException.php create mode 100755 lib/Rails/I18n/Exception/ExceptionInterface.php create mode 100755 lib/Rails/I18n/Exception/InvalidArgumentException.php create mode 100755 lib/Rails/I18n/Exception/RuntimeException.php create mode 100755 lib/Rails/I18n/I18n.php create mode 100755 lib/Rails/I18n/Locales/AbstractLocales.php create mode 100755 lib/Rails/I18n/Locales/En.php create mode 100755 lib/Rails/I18n/Locales/Es.php create mode 100755 lib/Rails/Loader/Exception/ClassNotFoundException.php create mode 100755 lib/Rails/Loader/Exception/ExceptionInterface.php create mode 100755 lib/Rails/Loader/Exception/FileNotFoundException.php create mode 100755 lib/Rails/Loader/Exception/RuntimeException.php create mode 100755 lib/Rails/Loader/Loader.php create mode 100755 lib/Rails/Log/Formatter/Simple.php create mode 100755 lib/Rails/Log/Logger.php create mode 100755 lib/Rails/Panel/assets/panel.css create mode 100755 lib/Rails/Panel/controllers/AdminController.php create mode 100755 lib/Rails/Panel/controllers/ApplicationController.php create mode 100755 lib/Rails/Panel/helpers/ApplicationHelper.php create mode 100755 lib/Rails/Panel/traits/AdminController.php create mode 100755 lib/Rails/Panel/traits/ApplicationController.php create mode 100755 lib/Rails/Panel/views/admin/compile_assets.php create mode 100755 lib/Rails/Panel/views/admin/create_files.php create mode 100755 lib/Rails/Panel/views/admin/forbidden.php create mode 100755 lib/Rails/Panel/views/admin/index.php create mode 100755 lib/Rails/Panel/views/admin/show_routes.php create mode 100755 lib/Rails/Panel/views/layouts/application.php create mode 100755 lib/Rails/Paths/Path.php create mode 100755 lib/Rails/Paths/Paths.php create mode 100755 lib/Rails/Rails.php create mode 100755 lib/Rails/Routing/Exception/BadMethodCallException.php create mode 100755 lib/Rails/Routing/Exception/ExceptionInterface.php create mode 100755 lib/Rails/Routing/Exception/InvalidArgumentException.php create mode 100755 lib/Rails/Routing/Exception/NotFoundException.php create mode 100755 lib/Rails/Routing/Exception/RoutingErrorException.php create mode 100755 lib/Rails/Routing/Exception/RuntimeException.php create mode 100755 lib/Rails/Routing/Mapper.php create mode 100755 lib/Rails/Routing/Route/HiddenRoute.php create mode 100755 lib/Rails/Routing/Route/PanelRoute.php create mode 100755 lib/Rails/Routing/Route/Route.php create mode 100755 lib/Rails/Routing/Route/RouteSet.php create mode 100755 lib/Rails/Routing/Router.php create mode 100755 lib/Rails/Routing/Traits/NamedPathAwareTrait.php create mode 100755 lib/Rails/Routing/UrlFor.php create mode 100755 lib/Rails/Routing/UrlHelpers/Exception/ExceptionInterface.php create mode 100755 lib/Rails/Routing/UrlHelpers/Exception/RuntimeException.php create mode 100755 lib/Rails/Routing/UrlHelpers/UrlHelpers.php create mode 100755 lib/Rails/Routing/UrlToken.php create mode 100755 lib/Rails/ServiceManager/Exception/ExceptionInterface.php create mode 100755 lib/Rails/ServiceManager/Exception/RuntimeException.php create mode 100755 lib/Rails/ServiceManager/ServiceAwareTrait.php create mode 100755 lib/Rails/ServiceManager/ServiceManager.php create mode 100755 lib/Rails/SystemExit/SystemExit.php create mode 100755 lib/Rails/Toolbox/ArrayTools.php create mode 100755 lib/Rails/Toolbox/ClassTools.php create mode 100755 lib/Rails/Toolbox/DbTools.php create mode 100755 lib/Rails/Toolbox/FileGenerators/AbstractGenerator.php create mode 100755 lib/Rails/Toolbox/FileGenerators/ControllerGenerator.php create mode 100755 lib/Rails/Toolbox/FileGenerators/Exception/ExceptionInterface.php create mode 100755 lib/Rails/Toolbox/FileGenerators/Exception/FileExistsException.php create mode 100755 lib/Rails/Toolbox/FileGenerators/Exception/FileNotCreatedException.php create mode 100755 lib/Rails/Toolbox/FileGenerators/GeneratorInterface.php create mode 100755 lib/Rails/Toolbox/FileGenerators/ModelGenerator.php create mode 100755 lib/Rails/Toolbox/FileTools.php create mode 100755 lib/Rails/Toolbox/functions.php create mode 100755 lib/Rails/Traits/ArrayObjectAccess.php create mode 100755 lib/Rails/Validation/Exception/ExceptionInterface.php create mode 100755 lib/Rails/Validation/Exception/InvalidArgumentException.php create mode 100755 lib/Rails/Validation/Exception/RuntimeException.php create mode 100755 lib/Rails/Validation/Validation.php create mode 100755 lib/Rails/Xml/Exception/ExceptionInterface.php create mode 100755 lib/Rails/Xml/Exception/InvalidArgumentException.php create mode 100755 lib/Rails/Xml/Xml.php create mode 100755 lib/Rails/Yaml/Parser.php create mode 100755 vendor/README create mode 100755 vendor/assets/stylesheets/bootstrap.css diff --git a/composer.json b/composer.json index 9ac10cf..e3c10fc 100755 --- a/composer.json +++ b/composer.json @@ -9,10 +9,7 @@ }, "autoload": { "psr-0": { - "Rails\\": "" + "Rails\\": "lib/Rails" } - }, - "config": { - "vendor-dir": "./.." } -} \ No newline at end of file +} diff --git a/lib/Rails/ActionController/ActionController.php b/lib/Rails/ActionController/ActionController.php new file mode 100755 index 0000000..9708457 --- /dev/null +++ b/lib/Rails/ActionController/ActionController.php @@ -0,0 +1,85 @@ +run_filters($type); + } + + static public function load_exception_handler($class_name, $status) + { + // $path = Rails::config()->paths->controllers; + // $file = $path . '/' . Rails::services()->get('inflector')->underscore($class_name) . '.php'; + // if (!is_file($file)) + // throw new ActionController_Exception(sprintf("File for exception handler controller not found. Searched for %s", $file)); + // require $file; + + // if (!class_exists($class_name, false)) + // throw new ActionController_Exception(sprintf("Class for exception handler controller not found. File: %s", $file)); + $handler = new $class_name(); + if (!$handler instanceof ExceptionHandler) { + throw new ActionController_Exception(sprintf("Exception handler class %s must extend %s. File: %s", $class_name, "ActionController\ExceptionHandler", $file)); + } + + $handler->class_name = $class_name; + $handler->setStatus((int)$status); + return $handler; + } + + public function params($name = null, $val = null) + { + if ($name) + $this->_dispatcher()->parameters()->$name = $val; + return $this->_dispatcher()->parameters(); + } + + public function request() + { + return $this->_dispatcher()->request(); + } + + public function response() + { + return Rails::application()->dispatcher()->response(); + } + + public function session() + { + return $this->_dispatcher()->session(); + } + + /** + * Retrieves Cookies instance, retrieves a cookie or + * sets a cookie. + */ + public function cookies($name = null, $val = null, array $params = array()) + { + $num = func_num_args(); + + if (!$num) + return $this->_dispatcher()->response()->cookies(); + elseif ($num == 1) + return $this->_dispatcher()->response()->cookies()->get($name); + else { + if ($val === null) + $this->_dispatcher()->response()->cookies()->delete($name); + else + return $this->_dispatcher()->response()->cookies()->add($name, $val, $params); + } + } + + protected function _dispatcher() + { + return Rails::application()->dispatcher(); + } +} \ No newline at end of file diff --git a/lib/Rails/ActionController/Base.php b/lib/Rails/ActionController/Base.php new file mode 100755 index 0000000..0520678 --- /dev/null +++ b/lib/Rails/ActionController/Base.php @@ -0,0 +1,808 @@ +isAppController($class)) { + $this->locals = new stdClass(); + $this->selfRefl = new ReflectionClass($class); + + if (!$this instanceof ExceptionHandler) { + $this->_set_default_layout(); + $reflection = $this->selfRefl; + + while (true) { + $parent = $reflection->getParentClass(); + if ($this->isAppController($parent->getName())) { + $this->appControllerRefls[] = $parent; + } elseif ($parent->getName() == __CLASS__) { + break; + } + $reflection = $parent; + } + $this->appControllerRefls = array_reverse($this->appControllerRefls); + $this->run_initializers(); + } + } + } + + public function __set($prop, $val) + { + $this->setLocal($prop, $val); + } + + public function __get($prop) + { + if (!array_key_exists($prop, (array)$this->locals)) { + throw new Exception\RuntimeException( + sprintf("Trying to get undefined local '%s'", $prop) + ); + } + return $this->locals->$prop; + } + + public function __call($method, $params) + { + if ($this->isNamedPathMethod($method)) { + return $this->getNamedPath($method, $params); + } + + throw new Exception\BadMethodCallException( + sprintf("Called to unknown method: %s", $method) + ); + } + + public function response_params() + { + return $this->_response_params; + } + + public function responded() + { + return $this->render_params || $this->redirect_params; + } + + public function setLocal($name, $value) + { + if (is_array($value)) { + $this->_array_names[] = $name; + $this->$name = $value; + } else { + $this->locals->$name = $value; + } + return $this; + } + + public function locals() + { + foreach ($this->_array_names as $prop_name) + $this->locals->$prop_name = $this->$prop_name; + return $this->locals; + } + + public function vars() + { + return $this->locals(); + } + + /** + * Shortcut + */ + public function I18n() + { + return Rails::services()->get('i18n'); + } + + public function action_name() + { + return Rails::services()->get('inflector')->camelize(Rails::application()->dispatcher()->router()->route()->action, true); + } + + public function run_request_action() + { + $action = $this->action_name(); + + if (!$this->_action_method_exists($action) && !$this->_view_file_exists()) { + throw new Exception\UnknownActionException( + sprintf("The action '%s' could not be found for %s", $action, get_called_class()) + ); + } + + $this->runFilters('before'); + /** + * Check if response params where set by the + * before filters. + */ + if (!$this->responded()) { + if ($this->_action_method_exists($action)) { + $this->actionRan = true; + $this->runAction($action); + } + $this->runFilters('after'); + } + + if ($this->redirect_params) { + $this->_set_redirection(); + } else { + $this->_parse_response_params(); + $this->_create_response_body(); + } + } + + public function actionRan() + { + return $this->actionRan; + } + + public function status() + { + return $this->status; + } + + /** + * This method was created so it can be extended in + * the controllers with the solely purpose of + * customizing the handle of Exceptions. + */ + protected function runAction($action) + { + $this->$action(); + } + + protected function init() + { + } + + /** + * Adds view helpers to the load list. + * Accepts one or many strings. + */ + public function helper() + { + ActionView\ViewHelpers::addAppHelpers(func_get_args()); + } + + /** + * Respond to format + * + * Sets to which formats the action will respond. + * It accepts a list of methods that will be called if the + * request matches the format. + * + * Example: + * $this->respondTo(array( + * 'html', + * 'xml' => array( + * '_some_method' => array($param_1, $param_2), + * '_render' => array(array('xml' => $obj), array('status' => 403)) + * ) + * )); + * + * Note: The way this function receives its parameters is because we can't use Closures, + * due to the protected visibility of the methods such as _render(). + * + * In the case above, it's stated that the action is able to respond to html and xml. + * In the case of an html request, no further action is needed to respond; therefore, + * we just list the 'html' format there. + * In the case of an xml request, the controller will call _some_method($param_1, $param_2) + * then it will call the _render() method, giving it the variable with which it will respond + * (an ActiveRecord_Base object, an array, etc), and setting the status to 403. + * Any request with a format not specified here will be responded with a 406 HTTP status code. + * + * By default all requests respond to xml, json and html. If the action receives a json + * request for example, but no data is set to respond with, the dispatcher will look for + * the .$format.php file in the views (in this case, .json.php). If the file is missing, + * which actually is expected to happen, a Dispatcher_TemplateMissing exception will be + * thrown. + * + * @see respond_with() + */ + public function respondTo($responses) + { + $format = $this->request()->format(); + + foreach ($responses as $fmt => $action) { + if (is_int($fmt)) { + $fmt = $action; + $action = null; + } + + if ($fmt !== $format) + continue; + + if ($action) { + if (!$action instanceof Closure) { + throw new Exception\InvalidArgumentException(sprinft('Only closure can be passed to respondTo, %s passed', gettype($action))); + } + $action(); + } else { + $action = true; + } + + $this->_respond_action = $action; + return; + } + + /** + * The request format is not acceptable. + * Set to render nothing with 406 status. + */ + Rails::log()->message("406 Not Acceptable"); + $this->render(array('nothing' => true), array('status' => 406)); + } + + public function respondWith($var) + { + $format = $this->request()->format(); + + if (!in_array($format, $this->respondTo)) { + /** + * The request format is not acceptable. + * Set to render nothing with 406 status. + */ + $this->render(array('nothing' => true), array('status' => 406)); + } else { + $responses = []; + foreach ($this->respondTo as $format) { + if ($format == 'html') { + $responses[] = 'html'; + } else { + $responses[$format] = function() use ($var, $format) { + $this->render([$format => $var]); + }; + } + } + $this->respondTo($responses); + } + } + + /** + * Sets layout value. + */ + public function layout($value = null) + { + if (null === $value) + return $this->layout; + else + $this->layout = $value; + } + + public function setLayout($value) + { + $this->layout = $value; + } + + protected function setRespondTo(array $respondTo) + { + $this->respondTo = $respondTo; + } + + /** + * @return array + */ + protected function filters() + { + return []; + } + + protected function render($render_params) + { + if ($this->responded()) { + throw new Exception\DoubleRenderException('Can only render or redirect once per action.'); + } + + $this->render_params = $render_params; + } + + /** + * Sets a redirection. + * Accepts ['status' => $http_status] among the $redirect_params. + */ + protected function redirectTo($redirect_params) + { + if ($this->responded()) { + throw new Exception\DoubleRenderException('Can only render or redirect once per action.'); + } + + $this->redirect_params = $redirect_params; + } + + /** + * For now we're only expecting one controller that extends ApplicationController, + * that extends ActionController_Base. + * This could change in the future (using Reflections) so there could be more classes + * extending down to ApplicationController > ActionController_Base + */ + protected function runFilters($type) + { + $closures = $this->getAppControllersMethod('filters'); + + if ($closures) { + $filters = []; + foreach ($closures as $closure) { + $filters = array_merge_recursive($closure(), $filters); + } + $filters = array_merge_recursive($filters, $this->filters()); + } else { + $filters = $this->filters(); + } + + if (isset($filters[$type])) { + /** + * We have to filter duped methods. We can't use array_unique + * because the the methods could be like 'method_name' => [ 'only' => [ actions ... ] ] + * and that will generate "Array to string conversion" error. + */ + $ranMethods = []; + + foreach ($filters[$type] as $methodName => $params) { + if (!is_array($params)) { + $methodName = $params; + $params = []; + } + + if ($this->canRunFilterMethod($params, $type) && !in_array($methodName, $ranMethods)) { + $this->$methodName(); + /** + * Before-filters may set response params. Running filters stop if one of them does. + */ + if ($type == 'before' && $this->responded()) { + break; + } + $ranMethods[] = $methodName; + } + } + } + } + + protected function setStatus($status) + { + $this->status = $status; + } + + private function _action_method_exists($action) + { + $method_exists = false; + $called_class = get_called_class(); + $refl = new ReflectionClass($called_class); + + if ($refl->hasMethod($action)) { + $method = $refl->getMethod($action); + if ($method->getDeclaringClass()->getName() == $called_class && $method->isPublic()) + $method_exists = true; + } + + return $method_exists; + } + + public function urlFor($params) + { + $urlfor = new UrlFor($params); + return $urlfor->url(); + } + + /** + * If the method for the requested action doesn't exist in + * the controller, it's checked if the view file exists. + */ + private function _view_file_exists() + { + $route = Rails::application()->dispatcher()->router()->route(); + /** + * Build a simple path for the view file, as there's no support for + * stuff like modules. + * Note that the extension is PHP, there's no support for different + * request formats. + */ + $base_path = Rails::config()->paths->views; + $view_path = $base_path . '/' . $route->path() . '.php'; + return is_file($view_path); + } + + private function _set_default_layout() + { + $this->layout(Rails::application()->config()->action_view->layout); + } + + private function canRunFilterMethod(array $params = [], $filter_type) + { + $action = Rails::services()->get('inflector')->camelize($this->request()->action(), false); + + if (isset($params['only']) && !in_array($action, $params['only'])) { + return false; + } elseif (isset($params['except']) && in_array($action, $params['except'])) { + return false; + } + return true; + } + + private function _parse_response_params() + { + if ($this->render_params) { + if (is_string($this->render_params)) { + $this->render_params = [ $this->render_params ]; + } + + if (isset($this->render_params[0])) { + $param = $this->render_params[0]; + unset($this->render_params[0]); + + if ($param == 'nothing') { + $this->render_params['nothing'] = true; + } elseif (is_int(($pos = strpos($param, '/'))) && $pos > 0) { + $this->render_params['template'] = $param; + } elseif ($pos === 0 || strpos($param, ':') === 1) { + $this->render_params['file'] = $param; + if (!isset($this->render_params['layout'])) { + $this->render_params['layout'] = false; + } + } else { + $this->render_params['action'] = $param; + } + } + // else { + + // } + } + // else { + // $route = Rails::application()->dispatcher()->router()->route(); + // $this->render_params = ['action' => $route->action]; + // } + } + + /* + * Protected so it can be accessed by ExceptionHandler + */ + protected function _create_response_body() + { + $route = Rails::application()->dispatcher()->router()->route(); + $render_type = false; + + foreach (self::$RENDER_OPTIONS as $option) { + if (isset($this->render_params[$option])) { + $render_type = $option; + $main_param = $this->render_params[$option]; + unset($this->render_params[$option]); + } + } + + if (!$render_type) { + $render_type = 'action'; + $main_param = $route->action; + } + + // $render_type = key($this->render_params); + // $main_param = array_shift($this->render_params); + + + if (isset($this->render_params['status'])) + $this->status = $this->render_params['status']; + + $class = null; + + switch ($render_type) { + case 'action': + # Cut the 'Controller' part of the class name. + $path = Rails::services()->get('inflector')->underscore(substr(get_called_class(), 0, -10)) . '/' . $main_param . '.php'; + + // if ($route->namespaces()) + // $path = implode('/', $route->namespaces()) . '/' . $path; + + $main_param = $path; + # Fallthrough + + case 'template': + $layout = !empty($this->render_params['layout']) ? $this->render_params['layout'] : $this->layout; + + $ext = pathinfo($main_param, PATHINFO_EXTENSION); + + if (!$ext) { + $template_name = $main_param; + + if ($this->request()->format() == 'html') { + $ext = 'php'; + } else { + if ($this->request()->format() == 'xml') + $this->_response_params['is_xml'] = true; + $ext = [$this->request()->format(), 'php']; + + $this->response()->headers()->contentType($this->request()->format()); + } + } else { + $pinfo = pathinfo($main_param); + if ($sub_ext = pathinfo($pinfo['filename'], PATHINFO_EXTENSION)) { + $pinfo['filename'] = substr($pinfo['filename'], 0, -1 * (1 + strlen($pinfo['filename']))); + $ext = [$sub_ext, $ext]; + } else { + + } + + $template_name = $pinfo['dirname'] . '/' . $pinfo['filename']; + } + + $this->_response_params = [ + 'layout' => $layout, + 'template_name' => $template_name, + 'extension' => $ext + ]; + + # Here we could choose a different responder according to extensions(?). + $class = 'Rails\ActionController\Response\Template'; + break; + + case 'lambda': + $class = 'Rails\ActionController\Response\Lambda'; + $this->_response_params['lambda'] = $main_param; + break; + + case 'partial': + $class = 'Rails\ActionController\Response\Partial'; + $this->_response_params['partial'] = $main_param; + break; + + case 'json': + $this->contentType = self::CONTENT_TYPE_JSON; + $this->_response_params = $main_param; + $class = "Rails\ActionController\Response\Json"; + break; + + case 'xml': + $this->contentType = self::CONTENT_TYPE_XML; + array_unshift($this->_response_params, $main_param); + $class = "Rails\ActionController\Response\Xml"; + break; + + case 'text': + $this->response()->body($main_param); + break; + + case 'inline': + $this->_response_params['code'] = $main_param; + $this->_response_params['layout'] = $this->layout; + $class = "Rails\ActionController\Response\Inline"; + break; + + case 'file': + $this->_response_params['file'] = $main_param; + $this->_response_params['layout'] = $this->layout ?: false; + $class = "Rails\ActionController\Response\File"; + break; + + case 'nothing': + break; + + default: + throw new Exception\RuntimeException(sprintf("Invalid action render type '%s'", $render_type)); + break; + } + + if ($class) { + $responder = new $class($this->_response_params); + $responder->render_view(); + $this->response()->body($responder->get_contents()); + } + + $this->setHeaders(); + } + + private function _set_redirection() + { + $redirect_params = $this->redirect_params; + + if (!is_array($redirect_params)) + $redirect_params = [$redirect_params]; + + if (!empty($redirect_params['status'])) { + $status = $redirect_params['status']; + unset($redirect_params['status']); + } else { + $status = self::DEFAULT_REDIRECT_STATUS; + } + + $url = Rails::application()->router()->urlFor($redirect_params); + + $this->response()->headers()->location($url); + $this->response()->headers()->status($status); + } + + /** + * Runs initializers for both the actual controller class + * and it's parent ApplicationController, if any. + */ + private function run_initializers() + { + $method_name = 'init'; + $cn = get_called_class(); + + # Run ApplicationController's init method. + if ($inits = $this->getAppControllersMethod($method_name)) { + foreach ($inits as $init) { + $init = $init->bindTo($this); + $init(); + } + } + + $method = $this->selfRefl->getMethod($method_name); + if ($method->getDeclaringClass()->getName() == $cn) { + $this->$method_name(); + } + } + + /** + * Searches through all the ApplicationControllers classes for a method, + * and returns them all. + * + * @return array + */ + private function getAppControllersMethod($methodName, $scope = '') + { + if ($this->appControllerRefls) { + $methods = []; + foreach ($this->appControllerRefls as $appRefl) { + if ($appRefl->hasMethod($methodName)) { + $method = $appRefl->getMethod($methodName); + + if ($this->isAppController($method->getDeclaringClass()->getName())) { + if ($scope) { + $isScope = 'is' . ucfirst($scope); + if (!$method->$isScope()) { + continue; + } + } + $methods[] = $method->getClosure($this); + } + } + } + + if ($methods) { + return $methods; + } + } + + return false; + } + + private function setHeaders() + { + $headers = $this->response()->headers(); + if (null === $this->charset) + $this->charset = Rails::application()->config()->action_controller->base->default_charset; + + if (!$headers->contentType()) { + if (null === $this->contentType) { + $this->contentType = 'text/html'; + } + $contentType = $this->contentType; + if ($this->charset) + $contentType .= '; charset=' . $this->charset; + + $headers->setContentType($contentType); + } + + $this->response()->headers()->status($this->status); + } + + private function isAppController($class) + { + return strpos($class, self::APP_CONTROLLER_CLASS) === (strlen($class) - strlen(self::APP_CONTROLLER_CLASS)); + } +} \ No newline at end of file diff --git a/lib/Rails/ActionController/Cookie.php b/lib/Rails/ActionController/Cookie.php new file mode 100755 index 0000000..5ceecc1 --- /dev/null +++ b/lib/Rails/ActionController/Cookie.php @@ -0,0 +1,96 @@ +setDefaultValues(); + + $this->name = (string)$name; + $this->domain = (string)$domain; + $this->value = $value === null ? '' : $value; + $this->expire = $expire; + $this->path = $path; + $this->secure = $secure; + $this->httponly = $httponly; + $this->raw = $raw; + + if (!$this->name) { + throw new Exception\InvalidArgumentException('Cookies must have a name'); + } + if (preg_match("/[=,; \t\r\n\013\014]/", $this->name)) { + throw new Exception\InvalidArgumentException("Cookie name cannot contain these characters: =,; \\t\\r\\n\\013\\014 ({$this->name})"); + } + if (is_string($this->expire)) { + $time = strtotime($this->expire); + if (!$time) { + throw new Exception\InvalidArgumentException( + sprintf("Invalid expiration time: %s", $time) + ); + } + $this->expire = $time; + } + if ($this->expire !== null && !is_int($this->expire)) { + throw new Exception\InvalidArgumentException( + sprintf("Cookie expiration time must be an integer, %s passed (%s)", gettype($this->expire), $this->expire) + ); + } + if ($this->raw && preg_match("/[=,; \t\r\n\013\014]/", $this->value)) { + throw new Exception\InvalidArgumentException( + sprintf("Raw cookie value cannot contain these characters: =,; \\t\\r\\n\\013\\014 (%s)", $this->value) + ); + } + if (!is_scalar($this->value)) { + throw new Exception\InvalidArgumentException( + sprintf("Cookie value must be a scalar value, %s passed", gettype($this->value)) + ); + } + } + + public function value() + { + return $this->value; + } + + public function set() + { + if ($this->raw) { + setrawcookie($this->name, $this->value, $this->expire, $this->path, $this->domain, $this->secure, $this->httponly); + } else { + setcookie($this->name, $this->value, $this->expire, $this->path, $this->domain, $this->secure, $this->httponly); + } + } + + private function setDefaultValues() + { + $config = Rails::application()->config()->cookies; + foreach ($config as $prop => $val) { + # Silently ignore unknown properties. + if (property_exists($this, $prop)) { + $this->$prop = $val; + } + } + } +} diff --git a/lib/Rails/ActionController/Cookies.php b/lib/Rails/ActionController/Cookies.php new file mode 100755 index 0000000..40a2a48 --- /dev/null +++ b/lib/Rails/ActionController/Cookies.php @@ -0,0 +1,94 @@ +cookies()'. + * + * Cookies can be set through this class. The cookies won't + * be actually sent to the browser until the controller + * finishes its work. + * + * Doing '$cookies->some_cookie' will call __get, + * which checks the $_COOKIE variable. In other words, it will + * check if the request sent a cookie named 'some_cookie'. + * If not, it will return null. + * + * To set a cookie, use add(), to remove them use remove(). + * To check if a cookie was set by the controller, use in_jar(). + */ +class Cookies +{ + /** + * Holds cookies that will be added at the + * end of the controller. + */ + private $jar = array(); + + /** + * To know if cookies were set or not. + */ + private $cookiesSet = false; + + public function __get($name) + { + if (isset($this->jar[$name])) + return $this->jar[$name]->value(); + elseif (isset($_COOKIE[$name])) + return $_COOKIE[$name]; + else + return null; + } + + public function __set($prop, $params) + { + if (is_array($params)) { + if (isset($params['value'])) { + $value = $params['value']; + unset($params['value']); + } else { + $value = ''; + } + } else { + $value = $params; + $params = []; + } + $this->add($prop, $value, $params); + } + + public function add($name, $value, array $params = array()) + { + $p = array_merge($this->defaultParams(), $params); + $this->jar[$name] = new Cookie($name, $value, $p['expires'], $p['path'], $p['domain'], $p['secure'], $p['httponly'], $p['raw']); + return $this; + } + + public function delete($name, array $params = []) + { + $this->add($name, '', array_merge($params, [ + 'expires' => time() - 172800 + ])); + return $this; + } + + /** + * Actually sets cookie in headers. + */ + public function set() + { + if (!$this->cookiesSet) { + foreach ($this->jar as $c) + $c->set(); + $this->cookiesSet = true; + } + } + + private function defaultParams() + { + $defaults = \Rails::application()->config()->cookies->toArray(); + if (!$defaults['path']) { + $defaults['path'] = \Rails::application()->router()->basePath() . '/'; + } + return $defaults; + } +} diff --git a/lib/Rails/ActionController/Exception/BadMethodCallException.php b/lib/Rails/ActionController/Exception/BadMethodCallException.php new file mode 100755 index 0000000..69284f9 --- /dev/null +++ b/lib/Rails/ActionController/Exception/BadMethodCallException.php @@ -0,0 +1,6 @@ +status()) { + case 404: + $this->template = '404'; + break; + + default: + $this->template = '500'; + break; + } + } + + public function handleException(\Exception $e) + { + $this->exception = $e; + $this->setLayout(false); + + $this->runAction("handle"); + + if (!$this->responded()) { + $this->render(['action' => $this->template]); + } + + $this->_create_response_body(); + } + + public function actionRan() + { + return true; + } +} diff --git a/lib/Rails/ActionController/Response.php b/lib/Rails/ActionController/Response.php new file mode 100755 index 0000000..5925662 --- /dev/null +++ b/lib/Rails/ActionController/Response.php @@ -0,0 +1,81 @@ +cookies) + $this->cookies = new Cookies(); + return $this->cookies; + } + + public function headers() + { + return \Rails::application()->dispatcher()->headers(); + } + + public function body($body = null) + { + if (null !== $body) { + $this->body = $body; + return $this; + } else { + return $this->body; + } + } + + public function sendHeaders($value = null) + { + if ($value === null) + return $this->send_headers; + else { + $this->send_headers = $value; + return $this; + } + } + + public function send_headers($value = null) + { + if ($value === null) + return $this->send_headers; + else { + $this->send_headers = $value; + return $this; + } + } + + protected function _init() + { + } + + protected function _respond() + { + $this->_send_headers(); + echo $this->body; + $this->body = null; + } + + private function _send_headers() + { + if ($this->send_headers) { + $this->headers()->send(); + $this->cookies()->set(); + } + } +} \ No newline at end of file diff --git a/lib/Rails/ActionController/Response/Base.php b/lib/Rails/ActionController/Response/Base.php new file mode 100755 index 0000000..66714f2 --- /dev/null +++ b/lib/Rails/ActionController/Response/Base.php @@ -0,0 +1,27 @@ +_params = $params; + } + + public function render_view() + { + $this->_render_view(); + return $this; + } + + public function get_contents() + { + return $this->_print_view(); + } + + abstract public function _render_view(); + + abstract public function _print_view(); +} \ No newline at end of file diff --git a/lib/Rails/ActionController/Response/Error.php b/lib/Rails/ActionController/Response/Error.php new file mode 100755 index 0000000..f9202bb --- /dev/null +++ b/lib/Rails/ActionController/Response/Error.php @@ -0,0 +1,94 @@ +_params = $params; + $this->_e = $e; + } + + public function _render_view() + { + $buffer = ''; + $this->_report = $this->_params['report']; + unset($this->_params['report']); + + if (\Rails::application()->config()->consider_all_requests_local) { + $no_html = \Rails::cli(); + + if ($no_html) { + $buffer .= strip_tags($this->_report); + $buffer .= "\n"; + } else { + $buffer .= $this->_header(); + $buffer .= $this->_report; + $buffer .= $this->_footer(); + } + } else { + $file = \Rails::publicPath() . '/' . $this->_params['status'] . '.html'; + if (is_file($file)) { + $buffer = file_get_contents($file); + } + } + + $this->_buffer = $buffer; + } + + public function _print_view() + { + return $this->_buffer; + } + + private function _header() + { +$h = << + + + + Exception caught + + + +HEREDOC; + return $h; + } + + private function _footer() + { + return "\n"; + } +} \ No newline at end of file diff --git a/lib/Rails/ActionController/Response/Exception/ActionNotFoundException.php b/lib/Rails/ActionController/Response/Exception/ActionNotFoundException.php new file mode 100755 index 0000000..cd2e995 --- /dev/null +++ b/lib/Rails/ActionController/Response/Exception/ActionNotFoundException.php @@ -0,0 +1,6 @@ +_params['layout']) ? $this->_params['layout'] : false; + + $this->_template = new ActionView\Template($this->_params['file'], ['layout' => $layout]); + + $this->_template->setLocals(\Rails::application()->controller()->vars()); + + $this->_template->renderContent(); + } + + public function _print_view() + { + return $this->_template->content(); + } +} \ No newline at end of file diff --git a/lib/Rails/ActionController/Response/Inline.php b/lib/Rails/ActionController/Response/Inline.php new file mode 100755 index 0000000..a875a80 --- /dev/null +++ b/lib/Rails/ActionController/Response/Inline.php @@ -0,0 +1,26 @@ +_params['layout']) ? $this->_params['layout'] : false; + # Create a template so we can call render_inline; + $this->_template = new ActionView\Template(['inline' => $this->_params['code']], ['layout' => $layout]); + $this->_template->setLocals(Rails::application()->controller()->vars()); + $this->_template->renderContent(); + } + + public function _print_view() + { + return $this->_template->content(); + } +} \ No newline at end of file diff --git a/lib/Rails/ActionController/Response/Json.php b/lib/Rails/ActionController/Response/Json.php new file mode 100755 index 0000000..493e05f --- /dev/null +++ b/lib/Rails/ActionController/Response/Json.php @@ -0,0 +1,55 @@ +_json = $json; + } + + public function _render_view() + { + if ($this->_json instanceof \Rails\ActiveRecord\Base) + $this->_json = $this->_json->toJson(); + elseif ($this->_json instanceof \Rails\ActiveRecord\Collection) { + $this->_json = $this->_json->toJson(); + } elseif (isset($this->_json['json'])) { + if (!is_string($this->_json['json'])) + $this->_json = json_encode($this->_json['json']); + else + $this->_json = $this->_json['json']; + } elseif (!is_string($this->_json)) { + if (is_array($this->_json)) { + $json = []; + foreach ($this->_json as $key => $val) { + $json[$key] = $this->_to_array($val); + } + $this->_json = $json; + } + $this->_json = json_encode($this->_json); + } + } + + public function _print_view() + { + return $this->_json; + } + + private function _to_array($val) + { + if ($val instanceof \Rails\ActiveRecord\Collection) { + $json = []; + foreach ($val as $obj) + $json[] = $this->_to_array($obj); + return $json; + } elseif (is_object($val)) { + return (array)$val; + } else + return $val; + } +} \ No newline at end of file diff --git a/lib/Rails/ActionController/Response/Lambda.php b/lib/Rails/ActionController/Response/Lambda.php new file mode 100755 index 0000000..55f726d --- /dev/null +++ b/lib/Rails/ActionController/Response/Lambda.php @@ -0,0 +1,25 @@ +_params['layout']) ? $this->_params['layout'] : false; + $this->_template = new ActionView\Template(['lambda' => $this->_params['lambda']], ['layout' => $layout]); + $this->_template->setLocals(\Rails::application()->controller()->locals()); + $this->_template->renderContent(); + } + + public function _print_view() + { + return $this->_template->content(); + } +} \ No newline at end of file diff --git a/lib/Rails/ActionController/Response/Partial.php b/lib/Rails/ActionController/Response/Partial.php new file mode 100755 index 0000000..0d7d2d3 --- /dev/null +++ b/lib/Rails/ActionController/Response/Partial.php @@ -0,0 +1,29 @@ +_params['partial']]; + if (isset($this->_params['locals'])) + $params = array_merge($params, [$this->_params['locals']]); + + # Include helpers. + ActionView\ViewHelpers::load(); + # Create a template so we can call render_partial. + # This shouldn't be done this way. + + $template = new ActionView\Template([]); + + $this->_body = call_user_func_array([$template, 'render_partial'], $params); + } + + public function _print_view() + { + return $this->_body; + } +} \ No newline at end of file diff --git a/lib/Rails/ActionController/Response/Redirect.php b/lib/Rails/ActionController/Response/Redirect.php new file mode 100755 index 0000000..93cec14 --- /dev/null +++ b/lib/Rails/ActionController/Response/Redirect.php @@ -0,0 +1,27 @@ +_redirect_params, $this->_header_params) = $redirect_params; + } + + protected function _render_view() + { + $url = \Rails::application()->router()->urlFor($this->_redirect_params); + \Rails::application()->dispatcher()->response()->headers()->location($url); + } + + protected function _print_view() + { + } +} \ No newline at end of file diff --git a/lib/Rails/ActionController/Response/Template.php b/lib/Rails/ActionController/Response/Template.php new file mode 100755 index 0000000..a6916ed --- /dev/null +++ b/lib/Rails/ActionController/Response/Template.php @@ -0,0 +1,85 @@ +build_template_file_name(); + + $params = [ + 'layout' => $this->_params['layout'] + ]; + + $this->renderer = new ActionView\Template($this->template_file_name, $params); + + $locals = Rails::application()->controller()->vars(); + + if (!empty($this->_params['is_xml'])) { + $this->_xml = new ActionView\Xml(); + $locals->xml = $this->_xml; + } + + $this->renderer->setLocals($locals); + + $this->renderer->renderContent(); + } catch (ActionView\Template\Exception\ExceptionInterface $e) { + switch (get_class($e)) { + case 'Rails\ActionView\Template\Exception\TemplateMissingException': + $route = Rails::application()->router()->route(); + if (Rails::application()->dispatcher()->controller()->actionRan()) { + throw new Exception\ViewNotFoundException( + sprintf("View file not found: %s", $this->template_file_name) + ); + } else { + if ($route->namespaces()) + $namespaces = ' [ namespaces => [ ' . implode(', ', $route->namespaces()) . ' ] ]'; + else + $namespaces = ''; + + throw new Exception\ActionNotFoundException( + // sprintf("Action '%s' not found for controller '%s'%s", $route->action(), $route->controller(), $namespaces) + sprintf("Action '%s' not found for controller '%s'%s", $route->action(), $route->controller(), $namespaces) + ); + } + break; + + // default: + // break; + } + throw $e; + } + } + + public function _print_view() + { + // if (!empty($this->_params['is_xml'])) + // return $this->_xml->output(); + // else + return $this->renderer->get_buffer_and_clean(); + } + + private function build_template_file_name() + { + $views_path = Rails::config()->paths->views; + + if (is_array($this->_params['extension'])) + $ext = implode('.', $this->_params['extension']); + else + $ext = $this->_params['extension']; + + $this->template_file_name = $views_path . DIRECTORY_SEPARATOR . $this->_params['template_name'] . '.' . $ext; + } +} \ No newline at end of file diff --git a/lib/Rails/ActionController/Response/View.php b/lib/Rails/ActionController/Response/View.php new file mode 100755 index 0000000..170d3ab --- /dev/null +++ b/lib/Rails/ActionController/Response/View.php @@ -0,0 +1,64 @@ +_renderer = new ActionView\Template($this->_params, $this->_params['layout']); + + $locals = Rails::application()->controller()->vars(); + + if (!empty($this->_params['is_xml'])) { + $this->_xml = new ActionView\Xml(); + $locals->xml = $this->_xml; + } + + $this->_renderer->setLocals($locals); + + $this->_renderer->render_content(); + } catch (ActionView\Template\Exception\ExceptionInterface $e) { + switch (get_class($e)) { + case 'Rails\ActionView\Template\Exception\LayoutMissingException': + case 'Rails\ActionView\Template\Exception\TemplateMissingException': + $route = $this->router()->route(); + if (Rails::application()->dispatcher()->action_ran()) { + $token = $route->to(); + throw new Exception\ViewNotFoundException( + sprintf("View for %s not found", $token) + ); + } else { + if ($route->namespaces()) + $namespaces = ' [ namespaces => [ ' . implode(', ', $route->namespaces()) . ' ] ]'; + else + $namespaces = ''; + + throw new Exception\ActionNotFoundException( + sprintf("Action '%s' not found for controller '%s'%s", $route->action(), $route->controller(), $namespace) + ); + } + break; + + default: + throw $e; + break; + } + } + } + + public function _print_view() + { + if (!empty($this->_params['is_xml'])) + return $this->_xml->output(); + else + return $this->_renderer->get_buffer_and_clean(); + } +} \ No newline at end of file diff --git a/lib/Rails/ActionController/Response/Xml.php b/lib/Rails/ActionController/Response/Xml.php new file mode 100755 index 0000000..b14de9d --- /dev/null +++ b/lib/Rails/ActionController/Response/Xml.php @@ -0,0 +1,29 @@ +_params); + + if ($el instanceof \Rails\ActiveRecord\Collection) { + $this->_xml = new \Rails\ActionView\Xml(); + $root = $this->_params['root']; + $this->_xml->instruct(); + $this->_xml->$root([], function() use ($el) { + foreach ($el as $model) { + $model->toXml(['builder' => $this->_xml, 'skip_instruct' => true]); + } + }); + } else + $this->_xml = new \Rails\Xml\Xml($el, $this->_params); + } + + public function _print_view() + { + return $this->_xml->output(); + } +} \ No newline at end of file diff --git a/lib/Rails/ActionDispatch/ActionDispatch.php b/lib/Rails/ActionDispatch/ActionDispatch.php new file mode 100755 index 0000000..eb32dfa --- /dev/null +++ b/lib/Rails/ActionDispatch/ActionDispatch.php @@ -0,0 +1,143 @@ +_response = new Response(); + $this->_router = new Routing\Router(); + $this->_response->_init(); + $this->_headers = new Http\Headers(); + $this->load_request_and_params(); + } + + public function load_request_and_params() + { + if (!$this->_parameters) { + $this->_request = new Request(); + $this->_parameters = new Http\Parameters(); + $this->_session = new Http\Session(); + } else { + throw new Exception\LogicException("Can't call init() more than once"); + } + } + + public function find_route() + { + $this->_router->find_route(); + $this->_route_vars_to_params(); + } + + public function router() + { + return $this->_router; + } + + /** + * This method shouldn't be accessed like Rails::application()->dispatcher()->parameters(); + */ + public function parameters() + { + return $this->_parameters; + } + + public function request() + { + return $this->_request; + } + + public function headers() + { + return $this->_headers; + } + + public function controller() + { + return Rails::application()->controller(); + } + + public function session() + { + return $this->_session; + } + + public function response() + { + return $this->_response; + } + + public function respond() + { + // if ($this->_responded && !Rails::response_params()) { + if ($this->_responded) { + throw new Exception\LogicException("Can't respond to request more than once"); + } else { + $this->_response->_respond(); + $this->_responded = true; + } + } + + // public function clear_responded() + // { + // static $cleared = false; + + // if ($cleared) { + // throw new Rails_ActionDispatch_Exception("Can't clear response more than once"); + // } else { + // $cleared = true; + // $this->_responded = false; + // } + // } + + private function _route_vars_to_params() + { + $vars = $this->_router->route()->vars(); + unset($vars['controller'], $vars['action']); + foreach ($vars as $name => $val) { + if ($this->_parameters->$name === null) + $this->_parameters->$name = $val; + } + } + + private function _action_name() + { + return $this->router()->route()->action; + } + + private function _action_exists() + { + $controller = Rails::application()->controller(); + return method_exists($controller, $this->_action_name()) && is_callable(array($controller, $this->_action_name())); + } + + private function _app() + { + return Rails::application(); + } +} \ No newline at end of file diff --git a/lib/Rails/ActionDispatch/Exception/ExceptionInterface.php b/lib/Rails/ActionDispatch/Exception/ExceptionInterface.php new file mode 100755 index 0000000..9d9f260 --- /dev/null +++ b/lib/Rails/ActionDispatch/Exception/ExceptionInterface.php @@ -0,0 +1,6 @@ +status; + // } + + public function status($status = null) + { + if (null === $status) { + return $this->status; + } else { + if (ctype_digit((string)$status)) + $this->status = (int)$status; + elseif (is_string($status)) + $this->status = $status; + else + throw new Exception\InvalidArgumentException( + sprintf("%s accepts string, %s passed.", __METHOD__, gettype($value)) + ); + return $this; + } + } + + public function location($url, $status = 302) + { + if (!is_string($url)) + throw new Exception\InvalidArgumentException( + sprintf("%s accepts string as first parameter, %s passed.", __METHOD__, gettype($value)) + ); + elseif (!is_int($status) && !is_string($status)) + throw new Exception\InvalidArgumentException( + sprintf("%s accepts string or int as second parameter, %s passed.", __METHOD__, gettype($status)) + ); + + $this->status($status)->set('Location', $url); + $this->status = $status; + return $this; + } + + public function set($name, $value = null) + { + return $this->add($name, $value); + } + + public function add($name, $value = null) + { + if (!is_string($name)) + throw new Exception\InvalidArgumentException( + sprintf("First argument for %s must be a string, %s passed.", __METHOD__, gettype($value)) + ); + elseif (!is_null($value) && !is_string($value) && !is_int($value)) + throw new Exception\InvalidArgumentException( + sprintf("%s accepts null, string or int as second argument, %s passed.", __METHOD__, gettype($value)) + ); + + if (strpos($name, 'Content-type') === 0) { + if ($value !== null) { + $name = $name . $value; + } + $this->contentType($name); + } elseif ($name == 'status') { + $this->status($value); + } elseif (strpos($name, 'HTTP/') === 0) { + $this->status($name); + } else { + if ($value === null) { + if (count(explode(':', $name)) < 2) + throw new Exception\InvalidArgumentException( + sprintf("%s is not a valid header", $name) + ); + $this->headers[] = $name; + } else { + $this->headers[$name] = $value; + } + } + return $this; + } + + public function send() + { + if ($this->status_sent) { + throw new Exception\LogicException("Headers have already been sent, can't send them twice"); + } + + if (!$this->_content_type) { + $this->_set_default_content_type(); + } + header($this->_content_type); + // vpe($this->_content_type); + foreach ($this->headers as $name => $value) { + if (!is_int($name)) + $value = $name . ': ' . $value; + header($value); + } + + if (is_int($this->status)) + header('HTTP/1.1 ' . $this->status); + else + header($this->status); + + $this->status_sent = true; + } + + public function contentType($content_type = null) + { + if (null === $content_type) { + return $this->_content_type; + } else { + return $this->setContentType($content_type); + } + } + + public function setContentType($content_type) + { + // static $i = 0; + + if (!is_string($content_type)) { + throw new Exception\InvalidArgumentException( + sprintf("Content type must be a string, %s passed", gettype($content_type)) + ); + } + // if($content_type != "text/html; charset=utf-8") + // if ($i) + // vpe($content_type); + switch ($content_type) { + case 'html': + $content_type = 'text/html'; + break; + + case 'json': + $content_type = 'application/json'; + break; + + case 'xml': + $content_type = 'application/xml'; + break; + } + + if (strpos($content_type, 'Content-type:') !== 0) + $content_type = 'Content-type: ' . $content_type; + + $this->_content_type = $content_type; + // $i++; + // vpe($content_type); + return $this; + } + + private function _set_default_content_type() + { + // $this->set_content_type('text/html; charset='.Rails::application()->config()->encoding); + } +} \ No newline at end of file diff --git a/lib/Rails/ActionDispatch/Http/Parameters.php b/lib/Rails/ActionDispatch/Http/Parameters.php new file mode 100755 index 0000000..cb371a2 --- /dev/null +++ b/lib/Rails/ActionDispatch/Http/Parameters.php @@ -0,0 +1,314 @@ +toArray()); + } + + public function __construct() + { + $method = \Rails::application()->dispatcher()->request()->method(); + if ($method != 'GET' && $method != 'POST') { + $params = file_get_contents('php://input'); + $decoded = []; + if (!empty($_SERVER['CONTENT_TYPE']) && $_SERVER['CONTENT_TYPE'] == "application/json") { + $decoded = json_decode($params, true); + if ($decoded === null) { + $decoded = []; + $this->_json_params_error = json_last_error(); + } + } else { + parse_str($params, $decoded); + } + + if ($method == 'DELETE') + $this->deleteVars = $decoded; + elseif ($method == 'PUT') + $this->putVars = $decoded; + elseif ($method == 'PATCH') + $this->patchVars = $decoded; + else + $this->other_params = $decoded; + } + + $this->_import_files(); + // vpe($this->files); + } + + public function __set($prop, $value) + { + if ($var = $this->_search($prop)) + global ${$var}; + + if (is_object($value)) { + if ($var) + $this->$prop = ${$var}[$prop]; + else + $this->$prop = $value; + } elseif (is_array($value)) { + if ($var) + $value = new GlobalVar($value, $var, $prop); + $this->$prop = $value; + } else { + if ($var) + ${$var}[$prop] = $value; + else + $this->$prop = $value; + } + } + + public function __get($prop) + { + $ret = null; + $var = $this->_search($prop); + if ($var) { + global ${$var}; + + if (is_array(${$var}[$prop])) { + // if (isset($this->files[$prop])) { + // ${$var}[$prop] = array_merge(${$var}[$prop], $this->files[$prop]); + // } + $this->$prop = new GlobalVar(${$var}[$prop], $var, $prop); + # Return here. + return $this->$prop; + } elseif (is_object(${$var}[$prop])) { + $this->$prop = ${$var}[$prop]; + $ret = $this->$prop; + } else { + $ret = ${$var}[$prop]; + } + } else { + if (isset($this->putVars[$prop])) + $ret = $this->putVars[$prop]; + elseif (isset($this->deleteVars[$prop])) + $ret = $this->deleteVars[$prop]; + elseif (isset($this->patchVars[$prop])) { + $ret = $this->patchVars[$prop]; + // elseif (isset($this->files[$prop])) { + # Return here. + // return $this->files[$prop]; + } elseif (isset($this->other_params[$prop])) + $ret = $this->other_params[$prop]; + } + + // if ($ret && $this->files) { + // vpe($this->files); + // $this->mergeWithFiles($ret, $prop); + + // } + + return $ret; + } + + public function __isset($prop) + { + return $this->_search($prop) || isset($this->deleteVars[$prop]) || isset($this->putVars[$prop]); + } + + public function del($prop) + { + unset($this->$prop, $_GET[$prop], $_POST[$prop], $this->deleteVars[$prop], $this->putVars[$prop]); + } + + public function get() + { + return $_GET; + } + + public function post() + { + return $_POST; + } + + public function delete() + { + return $this->deleteVars; + } + + public function put() + { + return $this->putVars; + } + + public function patch() + { + return $this->patchVars; + } + + public function files() + { + return $this->files; + } + + public function user() + { + get_object_vars($this); + } + + public function toArray() + { + $obj_vars = get_object_vars($this); + unset($obj_vars['deleteVars'], $obj_vars['putVars'], $obj_vars['_json_params_error'], $obj_vars['patchVars'], $obj_vars['other_params'], $obj_vars['files']); + + $ret = array_merge_recursive($_POST, $_GET, $obj_vars, $this->deleteVars, $this->putVars, $this->patchVars, $this->other_params/*, $this->files*/); + return $ret; + } + + public function all() + { + return $this->toArray(); + } + + public function merge() + { + $params = func_get_args(); + array_unshift($params, $this->all()); + return call_user_func_array('array_merge', $params); + } + + public function json_params_error() + { + return $this->_json_params_error; + } + + private function _search($prop) + { + if (isset($_GET[$prop])) + return '_GET'; + elseif (isset($_POST[$prop])) + return '_POST'; + else + return false; + } + + private function mergeWithFiles(&$array, $prop) + { + if (isset($this->files->$prop)) { + foreach ($this->files->$prop as $key => $value) { + if (is_array($value)) { + if (!isset($array[$key])) { + $array[$key] = []; + } elseif (!is_array($array[$key])) { + $array[$key] = [ $array[$key] ]; + } + $array[$key] = array_merge($array[$key], $value); + } else { + $array[$key] = $value; + } + } + } + } + + private function _import_files() + { + if (empty($_FILES)) { + return; + } + + $this->files = new \stdClass(); + + foreach ($_FILES as $mainName => $data) { + if (!is_array($data['name']) && $data['error'] != UPLOAD_ERR_NO_FILE) { + $this->files->$mainName = new UploadedFile($_FILES[$mainName]); + } else { + $this->files->$mainName = $this->_get_subnames($data); + } + } + } + + private function _get_subnames(array $arr) + { + $arranged = new \ArrayObject(); + // $arranged = []; + + foreach ($arr['name'] as $k => $value) { + if (is_string($value)) { + if ($arr['error'] != UPLOAD_ERR_NO_FILE) { + $arranged[$k] = [ + 'name' => $value, + 'type' => $arr['type'][$k], + 'tmp_name' => $arr['tmp_name'][$k], + 'error' => $arr['error'][$k], + 'size' => $arr['size'][$k], + ]; + } + } else { + $keys = ['name', $k]; + $this->_get_subnames_2($arranged, $keys, $arr); + } + } + + return $arranged->getArrayCopy(); + } + + private function _get_subnames_2($arranged, $keys, $arr) + { + $baseArr = $arr; + foreach ($keys as $key) { + $baseArr = $baseArr[$key]; + } + + foreach ($baseArr as $k => $value) { + if (is_string($value)) { + $this->setArranged($arranged, array_merge($keys, [$k]), [ + 'name' => $value, + 'type' => $this->foreachKeys(array_merge(['type'] + $keys, [$k]), $arr), + 'tmp_name' => $this->foreachKeys(array_merge(['tmp_name'] + $keys, [$k]), $arr), + 'error' => $this->foreachKeys(array_merge(['error'] + $keys, [$k]), $arr), + 'size' => $this->foreachKeys(array_merge(['size'] + $keys, [$k]), $arr), + ]); + // vpe($arranged, $key, $k); + // $arranged[$k] = $arranged[$k]->getArrayCopy(); + } else { + $tmpKeys = $keys; + $tmpKeys[] = $k; + $this->_get_subnames_2($arranged, $tmpKeys, $arr); + } + } + } + + private function foreachKeys($keys, $arr) + { + $baseArr = $arr; + foreach ($keys as $key) { + $baseArr = $baseArr[$key]; + } + return $baseArr; + } + + private function setArranged($arr, $keys, $val) + { + if ($val['error'] == UPLOAD_ERR_NO_FILE) { + return; + } + + array_shift($keys); + $lastKey = array_pop($keys); + $baseArr = &$arr; + foreach ($keys as $key) { + if (!isset($baseArr[$key])) { + // $baseArr[$key] = new \ArrayObject(); + $baseArr[$key] = []; + } + $baseArr = &$baseArr[$key]; + } + $baseArr[$lastKey] = new UploadedFile($val); + } +} diff --git a/lib/Rails/ActionDispatch/Http/Session.php b/lib/Rails/ActionDispatch/Http/Session.php new file mode 100755 index 0000000..8ae8e3c --- /dev/null +++ b/lib/Rails/ActionDispatch/Http/Session.php @@ -0,0 +1,64 @@ +set($prop, $value); + } + + public function __get($prop) + { + return $this->get($prop); + } + + public function set($prop, $value) + { + if (is_object($value)) { + $this->$prop = $value; + $_SESSION[$prop] = $value; + } elseif (is_array($value)) { + $arr = new GlobalVar($value, '_SESSION', $prop); + $this->$prop = $arr; + } else { + $_SESSION[$prop] = $value; + } + return $this; + } + + public function get($prop) + { + if (isset($_SESSION[$prop])) { + if (is_array($_SESSION[$prop])) { + $this->$prop = new GlobalVar($_SESSION[$prop], '_SESSION', $prop); + return $this->$prop; + } elseif (is_object($_SESSION[$prop])) { + $this->$prop = $_SESSION[$prop]; + return $this->$prop; + } else { + return $_SESSION[$prop]; + } + } + return null; + } + + public function delete($prop) + { + unset($this->$prop, $_SESSION[$prop]); + } + + public function merge() + { + $params = func_get_args(); + array_unshift($params, $_SESSION); + return call_user_func_array('array_merge', $params); + } +} \ No newline at end of file diff --git a/lib/Rails/ActionDispatch/Http/UploadedFile.php b/lib/Rails/ActionDispatch/Http/UploadedFile.php new file mode 100755 index 0000000..3873594 --- /dev/null +++ b/lib/Rails/ActionDispatch/Http/UploadedFile.php @@ -0,0 +1,59 @@ +name = $data['name']; + $this->type = $data['type']; + $this->tempName = $data['tmp_name']; + $this->error = $data['error']; + $this->size = $data['size']; + } + + public function name() + { + return $this->name; + } + + public function type() + { + return $this->type; + } + + public function tempName() + { + return $this->tempName; + } + + public function size() + { + return $this->size; + } + + public function errorCode() + { + return $this->error; + } + + public function error() + { + return !($this->error == UPLOAD_ERR_OK); + } + + public function move($newName) + { + return move_uploaded_file($this->tempName, $newName); + } +} \ No newline at end of file diff --git a/lib/Rails/ActionDispatch/Request.php b/lib/Rails/ActionDispatch/Request.php new file mode 100755 index 0000000..4bbffe1 --- /dev/null +++ b/lib/Rails/ActionDispatch/Request.php @@ -0,0 +1,175 @@ +originalPath(), strlen(\Rails::application()->router()->basePath())); + } + + /** + * Request path without the query string. + * The application's basePath is included. + * + * @return string + */ + public function originalPath() + { + if (is_int($pos = strpos($this->get('REQUEST_URI'), '?'))) { + return substr($this->get('REQUEST_URI'), 0, $pos); + } + return substr($this->get('REQUEST_URI'), 0); + } + + /** + * Full request path, includes query string, but excludes basePath. + * + * @return string + */ + public function fullPath() + { + return substr($this->get('REQUEST_URI'), strlen(\Rails::application()->router()->basePath())); + } + + /** + * Full request path, includes both basePath and query string. + * + * @return string + */ + public function originalFullPath() + { + return $this->get('REQUEST_URI'); + } + + public function controller() + { + if (!($router = \Rails::application()->dispatcher()->router()) || !($route = $router->route())) + return false; + return $route->controller; + } + + public function action() + { + if (!($router = \Rails::application()->dispatcher()->router()) || !($route = $router->route())) + return false; + return $route->action; + } + + public function isGet() + { + return $this->method() === 'GET'; + } + + public function isPost() + { + return $this->method() == 'POST'; + } + + public function isPut() + { + return $this->method() == 'PUT'; + } + + public function isDelete() + { + return $this->method() == 'DELETE'; + } + + public function isPatch() + { + return $this->method() == 'PATCH'; + } + + /** + * Checks the request method. + */ + public function is($method) + { + $method = strtoupper($method); + return $this->method() == $method; + } + + public function isLocal() + { + return \Rails::config()->consider_all_requests_local ?: $this->remoteIp() == self::LOCALHOST; + } + + public function remoteIp() + { + if ($this->get('HTTP_CLIENT_IP')) + $remoteIp = $this->get('HTTP_CLIENT_IP'); + elseif ($this->get('HTTP_X_FORWARDED_FOR')) + $remoteIp = $this->get('HTTP_X_FORWARDED_FOR'); + else + $remoteIp = $this->get('REMOTE_ADDR'); + return $remoteIp; + } + + /** + * Returns the overridden method name. + * + * @return string + */ + public function method() + { + if (isset($_POST['_method'])) { + $method = strtoupper($_POST['_method']); + if (in_array($method, self::$allowedHackMethods)) { + return $method; + } + } + return $this->get('REQUEST_METHOD'); + } + + public function protocol() + { + $protocol = ($val = $this->get('HTTPS')) && $val !== 'off' ? 'https' : 'http'; + return $protocol . '://'; + } + + public function isXmlHttpRequest() + { + return (($var = $this->get("HTTP_X_REQUESTED_WITH"))) && $var === "XMLHttpRequest"; + } + + public function format() + { + if ($route = \Rails::application()->dispatcher()->router()->route()) + return $route->format; + } + + /** + * Get an index in the $_SERVER superglobal. + * + * @return null|string + */ + public function get($name) + { + $name = strtoupper($name); + if (isset($_SERVER[$name])) { + return $_SERVER[$name]; + } + return null; + } +} \ No newline at end of file diff --git a/lib/Rails/ActionMailer/ActionMailer.php b/lib/Rails/ActionMailer/ActionMailer.php new file mode 100755 index 0000000..2abd028 --- /dev/null +++ b/lib/Rails/ActionMailer/ActionMailer.php @@ -0,0 +1,147 @@ +paths->mailers; + $file = $mails_path . '/' . Rails::services()->get('inflector')->underscore($name) . '.php'; + if (!is_file($file)) { + if ($raise_exception) + throw new Exception\RuntimeException( + sprintf('No file found for mailer %s (searched in %s)', $name, $file) + ); + return false; + } + + require $file; + + if (!class_exists($name, false)) { + if ($raise_exception) + throw new Exception\RuntimeException( + sprintf("File for mailer %s doesn't contain expected class", $name) + ); + return false; + } + } + + self::init(); + return true; + } + + static public function transport(Mail\Transport\TransportInterface $transport = null) + { + if (null !== $transport) { + self::$transport = $transport; + } elseif (!self::$transport) { + self::setDefaultTransport(); + } + return self::$transport; + } + + static public function filenameGenerator() + { + return 'action_mailer_' . $_SERVER['REQUEST_TIME'] . '_' . mt_rand() . '.tmp'; + } + + static protected function setDefaultTransport() + { + $config = Rails::application()->config()->action_mailer; + + switch ($config['delivery_method']) { + /** + * Rails to Zend options: + * address => name + * domain => host + * port => port + * authentication => connection_class + * user_name => connection_config[username] + * password => connection_config[password] + * enable_starttls_auto (true) => connection_config[ssl] => 'tls' + * enable_starttls_auto (false) => connection_config[ssl] => 'ssl' + */ + case 'smtp': + $defaultConfig = [ + 'address' => '127.0.0.1', + 'domain' => 'localhost', + 'port' => 25, + 'user_name' => '', + 'password' => '', + + 'enable_starttls_auto' => true, + + /** + * Differences with RoR: + * - ZF2 adds the "smtp" option + * - The "cram_md5" option is called "crammd5" + */ + 'authentication' => 'login' + ]; + + $smtp = array_merge($defaultConfig, $config['smtp_settings']->toArray()); + + $options = [ + 'host' => $smtp['address'], + 'name' => $smtp['domain'], + 'port' => $smtp['port'], + 'connection_class' => $smtp['authentication'], + 'connection_config' => [ + 'username' => $smtp['user_name'], + 'password' => $smtp['password'], + 'ssl' => $smtp['enable_starttls_auto'] ? 'tls' : null, + ], + ]; + + $options = new Mail\Transport\SmtpOptions($options); + + $transport = new Mail\Transport\Smtp(); + + $transport->setOptions($options); + break; + + /** + * location => path + * name_generator => callback + */ + case 'file': + $customOpts = $config['file_settings']; + $options = []; + + if ($customOpts['location'] === null) { + $dir = Rails::root() . '/tmp/mail'; + if (!is_dir($dir)) + mkdir($dir, 0777, true); + $customOpts['location'] = $dir; + } + + $options['path'] = $customOpts['location']; + + if ($customOpts['name_generator'] === null) { + $options['callback'] = 'Rails\ActionMailer\ActionMailer::filenameGenerator'; + } else { + $options['callback'] = $customOpts['name_generator']; + } + + $fileOptions = new Mail\Transport\FileOptions($options); + + $transport = new Mail\Transport\File(); + + $transport->setOptions($fileOptions); + + break; + + case ($config['delivery_method'] instanceof Closure): + $transport = $config['delivery_method'](); + break; + } + + self::transport($transport); + } +} \ No newline at end of file diff --git a/lib/Rails/ActionMailer/Base.php b/lib/Rails/ActionMailer/Base.php new file mode 100755 index 0000000..66fcd32 --- /dev/null +++ b/lib/Rails/ActionMailer/Base.php @@ -0,0 +1,135 @@ +calledMethod = $method; + $mailer->headers = array_merge($mailer->headers, $headers); + if (false !== call_user_func_array([$mailer, $method], $params)) + return $mailer->createMail(); + } + + public function __construct() + { + $this->vars = new stdClass(); + $this->init(); + } + + public function __set($prop, $value) + { + $this->vars->$prop = $value; + } + + public function __get($prop) + { + if (!isset($this->vars->$prop)) + return null; + return $this->vars->$prop; + } + + /** + * Just a quicker way to add an attachment. + */ + public function attachment($name, $content) + { + if (!is_string($content) && (!is_resource($content) || get_resource_type($content) != 'stream')) { + throw new Exception\InvalidArgumentException( + sprintf("Attachment content must be either string or stream, %s passed") + ); + } + + $this->attachments[$name] = [ + 'content' => $content + ]; + } + + /** + * Just a quicker way to add an inline attachment. + */ + public function inlineAttachment($name, $content) + { + if (!is_string($content) && (!is_resource($content) || get_resource_type($content) != 'stream')) { + throw new Exception\InvalidArgumentException( + sprintf("Attachment content must be either string or stream, %s passed") + ); + } + + $this->attachments[$name] = [ + 'content' => $content, + 'inline' => true + ]; + } + + public function vars() + { + return $this->vars; + } + + /** + * Default values for properties can be set in this method. + */ + protected function init() + { + } + + private function createMail() + { + if (Rails::config()->action_mailer->defaults) { + if (!Rails::config()->action_mailer->defaults instanceof \Closure) { + throw new Exception\RuntimeException( + 'Configuration action_mailer.defaults must be a closure' + ); + } + $closure = clone Rails::config()->action_mailer->defaults; + $closure = $closure->bindTo($this); + $closure(); + } + + if (!$this->templateName) + $this->templateName = $this->calledMethod; + + if (!$this->templatePath) + $this->templatePath = Rails::services()->get('inflector')->underscore(get_called_class()); + + $deliverer = new Deliverer($this); + return $deliverer; + } +} diff --git a/lib/Rails/ActionMailer/Deliverer.php b/lib/Rails/ActionMailer/Deliverer.php new file mode 100755 index 0000000..184a333 --- /dev/null +++ b/lib/Rails/ActionMailer/Deliverer.php @@ -0,0 +1,230 @@ +mail = $mail; + $this->buildMessage(); + } + + public function deliver() + { + ActionMailer::transport()->send($this->message); + return $this; + } + + public function mail() + { + return $this->mail; + } + + public function message() + { + return $this->message; + } + + private function buildMessage() + { + $this->message = new Mail\Message(); + + $this->setCharset(); + $this->setFrom(); + $this->setTo(); + $this->setSubject(); + + ActionView\ViewHelpers::load(); + + $this->createTextPart(); + $this->createHtmlPart(); + + $this->body = new Mime\Message(); + + $this->addTemplates(); + $this->addAttachments(); + + $this->message->setBody($this->body); + + unset($this->textTemplate, $this->htmlTemplate); + } + + private function setCharset() + { + if (!$charset = $this->mail->charset) { + $charset = mb_detect_encoding($this->mail->subject); + if (!$charset) + $charset = null; + } + + $this->message->setEncoding($charset); + } + + private function setFrom() + { + if (!is_array($this->mail->from)) { + $email = $this->mail->from; + $name = null; + } else { + list($email, $name) = $this->mail->from; + } + + $this->message->setFrom($email, $name); + } + + private function setTo() + { + $this->message->addTo($this->mail->to); + } + + private function setSubject() + { + $this->message->setSubject($this->mail->subject); + } + + private function createTextPart() + { + $template_file = $this->templateBasename() . '.text.php'; + try { + $template = new Template($template_file); + $template->setLocals($this->mail->vars()); + $this->textTemplate = $template; + } catch (Exception\ExceptionInterface $e) { + } + } + + private function createHtmlPart() + { + $template_file = $this->templateBasename() . '.php'; + try { + $template = new Template($template_file); + $template->setLocals($this->mail->vars()); + $this->htmlTemplate = $template; + } catch (Exception\ExceptionInterface $e) { + } + } + + private function templateBasename() + { + return Rails::config()->paths->views . '/' . + $this->mail->templatePath . '/' . + $this->mail->templateName; + } + + private function addTemplates() + { + if ($this->textTemplate) { + $content = $this->textTemplate->renderContent(); + $part = new Mime\Part($content); + $part->type = 'text/plain'; + $part->encoding = Mime\Mime::ENCODING_QUOTEDPRINTABLE; + $this->body->addPart($part); + } + + if ($this->htmlTemplate) { + $content = $this->htmlTemplate->renderContent(); + $part = new Mime\Part($content); + $part->type = 'text/html'; + $part->encoding = Mime\Mime::ENCODING_QUOTEDPRINTABLE; + $this->body->addPart($part); + } + } + + /** + * Requires Fileinfo. + */ + private function addAttachments() + { + if (class_exists('Finfo', false)) { + $finfo = new \Finfo(FILEINFO_MIME_TYPE); + } else { + $finfo = false; + } + + foreach ($this->mail->attachments as $filename => $attachment) { + if (!is_array($attachment)) { + throw new Exception\RuntimeException( + sprintf("Attachments must be array, %s passed", gettype($attachment)) + ); + } elseif ( + !is_string($attachment['content']) && + ( + !is_resource($attachment['content']) || + !get_resource_type($attachment['content']) == 'stream' + ) + ) { + throw new Exception\RuntimeException( + sprintf( + "Attachment content must be string or stream, %s passed", + gettype($attachment['content']) + ) + ); + } + + $type = null; + + if (empty($attachment['mime_type']) && $finfo) { + if (is_resource($attachment['content'])) { + $type = $finfo->buffer(stream_get_contents($attachment['content'])); + rewind($attachment['content']); + } else { + $type = $finfo->buffer($attachment['content']); + } + } + + $part = new Mime\Part($attachment['content']); + + if (empty($attachment['encoding'])) { + $attachment['encoding'] = Mime\Mime::ENCODING_BASE64; + } + + $part->encoding = $attachment['encoding']; + + if ($type) { + $part->type = $type; + } + + $part->disposition = !empty($attachment['inline']) ? + Mime\Mime::DISPOSITION_INLINE : + Mime\Mime::DISPOSITION_ATTACHMENT; + + $this->body->addPart($part); + } + } +} \ No newline at end of file diff --git a/lib/Rails/ActionMailer/Exception/ExceptionInterface.php b/lib/Rails/ActionMailer/Exception/ExceptionInterface.php new file mode 100755 index 0000000..24b04f3 --- /dev/null +++ b/lib/Rails/ActionMailer/Exception/ExceptionInterface.php @@ -0,0 +1,6 @@ +_template_file = $template_file; + } + + public function renderContent() + { + ob_start(); + require $this->_template_file; + $this->_contents = ob_get_clean(); + return $this->_contents; + } + + public function contents() + { + return $this->_contents; + } +} \ No newline at end of file diff --git a/lib/Rails/ActionView/ActionView.php b/lib/Rails/ActionView/ActionView.php new file mode 100755 index 0000000..1c2f09c --- /dev/null +++ b/lib/Rails/ActionView/ActionView.php @@ -0,0 +1,88 @@ +_add_content_for($name, ob_get_clean(), $prefix); + } + + public function provide($name, $content) + { + $this->_add_content_for($name, $content, false); + } + + public function clear_content_for($name) + { + unset(self::$_content_for[$name]); + } + + /** + * Yield seems to be a reserved keyword in PHP 5.5. + * Forced to change the name of the yield method to "content". + */ + public function content($name = null) + { + if ($name && isset(self::$_content_for[$name])) + return self::$_content_for[$name]; + } + + /** + * Passing a closure to do_content_for() will cause it + * to do the same as end_content_for(): add the buffered + * content. Thus, this method. + * + * @param string $name content's name + * @param string $value content's body + * @param bool $prefix to prefix or not the value to the current value + */ + private function _add_content_for($name, $value, $prefix) + { + !array_key_exists($name, self::$_content_for) && self::$_content_for[$name] = ''; + if ($prefix) + self::$_content_for[$name] = $value . self::$_content_for[$name]; + else + self::$_content_for[$name] .= $value; + } +} \ No newline at end of file diff --git a/lib/Rails/ActionView/Base.php b/lib/Rails/ActionView/Base.php new file mode 100755 index 0000000..2cd74cb --- /dev/null +++ b/lib/Rails/ActionView/Base.php @@ -0,0 +1,204 @@ +getLocal($prop); + } + + public function __set($prop, $val) + { + if (!$this->locals) { + $this->locals = new stdClass(); + } + + $this->setLocal($prop, $val); + } + + public function __call($method, $params) + { + if ($helper = ViewHelpers::findHelperFor($method)) { + $helper->setView($this); + return call_user_func_array(array($helper, $method), $params); + } elseif ($this->isNamedPathMethod($method)) { + return $this->getNamedPath($method, $params); + } + + throw new Exception\BadMethodCallException( + sprintf("Called to unknown method/helper: %s", $method) + ); + } + + # Remove for 2.0 + public function __isset($prop) + { + return $this->localExists($prop); + } + + public function localExists($name) + { + if ($this->locals instanceof stdClass) { + return property_exists($this->locals, $name); + } else { + return array_key_exists($name, $this->locals); + } + } + + public function getLocal($name) + { + if (!$this->localExists($name)) { + throw new Exception\RuntimeException( + sprintf("Undefined local '%s'", $name) + ); + } + + if ($this->locals instanceof stdClass) { + return $this->locals->$name; + } else { + return $this->locals[$name]; + } + } + + public function setLocal($name, $value) + { + if ($this->locals instanceof stdClass) { + $this->locals->$name = $value; + } else { + $this->locals[$name] = $value; + } + return $this; + } + + // public function isset_local($name) + // { + // if ($this->locals instanceof stdClass) + // return property_exists($this->locals, $name); + // elseif (is_array($this->locals)) + // return array_key_exists($name, $this->locals); + // } + + public function I18n() + { + return Rails::application()->I18n(); + } + + public function t($name, array $params = []) + { + $trans = $this->I18n()->t($name, $params); + if (false === $trans) { + return '#' . $name . ''; + } + return $trans; + } + + /** + * This method could go somewhere else. + */ + public function optionsFromEnumColumn($model_name, $column_name, array $extra_options = []) + { + $options = []; + foreach ($model_name::table()->enumValues($column_name) as $val) { + $options[$this->humanize($val)] = $val; + } + + if ($extra_options) { + $options = array_merge($extra_options, $options); + } + + return $options; + } + + /** + * This is meant to be a way to check if + * there are contentFor awaiting to be ended. + */ + public function activeContentFor() + { + return self::$_content_for_names; + } + + public function setLocals($locals) + { + if (!is_array($locals) && !$locals instanceof stdClass) + throw new Exception\InvalidArgumentException( + sprintf('Locals must be either an array or an instance of stdClass, %s passed.', gettype($locals)) + ); + $this->locals = $locals; + } + + public function params() + { + return Rails::application()->dispatcher()->parameters(); + } + + public function request() + { + return Rails::application()->dispatcher()->request(); + } + + public function partial($name, array $locals = array()) + { + $ctrlr_name = Rails::services()->get('inflector')->camelize($this->request()->controller(), false); + // $ctrlr_name = substr_replace(Rails::services()->get('inflector')->camelize($ctrlr_name), substr($ctrlr_name, 0, 1), 0, 1); + + if (!isset($locals[$ctrlr_name]) && $this->localExists($ctrlr_name)) { + $locals[$ctrlr_name] = $this->getLocal($ctrlr_name); + } + + // if (!Rails::config()->ar2) { + // if (!isset($locals[$name]) && $this->localExists($name)) { + // $locals[$name] = $this->getLocal($name); + // } + // } else { + $camelized = Rails::services()->get('inflector')->camelize($name, false); + if (!isset($locals[$camelized]) && $this->localExists($camelized)) { + $locals[$camelized] = $this->getLocal($camelized); + } + // } + + $base_path = Rails::config()->paths->views; + + if (is_int(strpos($name, '/'))) { + $pos = strrpos($name, '/'); + $name = substr_replace($name, '/_', $pos, 1) . '.php'; + $filename = $base_path . '/' . $name; + } else { + if ($namespaces = Rails::application()->dispatcher()->router()->route()->namespaces()) + $base_path .= '/' . implode('/', $namespaces); + $filename = $base_path . '/' . $this->request()->controller() . '/_' . $name . '.php'; + } + + if (isset($locals['collection'])) { + $collection = $locals['collection']; + unset($locals['collection']); + $contents = ''; + foreach ($collection as $member) { + $locals[$name] = $member; + $contents .= (new Partial($filename, [], $locals))->render_content(); + } + } else { + $partial = new Partial($filename, [], $locals); + $contents = $partial->render_content(); + } + + return $contents; + } +} diff --git a/lib/Rails/ActionView/Exception/BadMethodCallException.php b/lib/Rails/ActionView/Exception/BadMethodCallException.php new file mode 100755 index 0000000..de77b35 --- /dev/null +++ b/lib/Rails/ActionView/Exception/BadMethodCallException.php @@ -0,0 +1,6 @@ +helper = $helper; + $this->model = $model; + $this->inputNamespace = \Rails::services()->get('inflector')->underscore(get_class($model)); + } + + public function textField($property, array $attrs = array()) + { + $this->helper->setDefaultModel($this->model); + return $this->helper->textField($this->inputNamespace, $property, $attrs); + } + + public function hiddenField($property, array $attrs = array()) + { + $this->helper->setDefaultModel($model); + return $this->helper->hiddenField($this->inputNamespace, $property, $attrs); + } + + public function passwordField($property, array $attrs = array()) + { + $this->helper->setDefaultModel($model); + return $this->helper->passwordField($this->inputNamespace, $property, $attrs); + } + + public function checkBox($property, array $attrs = array(), $checked_value = '1', $unchecked_value = '0') + { + $this->helper->setDefaultModel($model); + return $this->helper->passwordField($this->inputNamespace, $property, $attrs, $checked_value, $unchecked_value); + } + + public function textArea($property, array $attrs = array()) + { + $this->helper->setDefaultModel($model); + return $this->helper->textArea($this->inputNamespace, $property, $attrs); + } + + public function select($property, $options, array $attrs = array()) + { + $this->helper->setDefaultModel($model); + return $this->helper->select($this->inputNamespace, $property, $options, $attrs); + } + + public function radioButton($property, $tag_value, array $attrs = array()) + { + $this->helper->setDefaultModel($model); + return $this->helper->radioButton($this->inputNamespace, $property, $tag_value, $attrs); + } + +} diff --git a/lib/Rails/ActionView/Helper.php b/lib/Rails/ActionView/Helper.php new file mode 100755 index 0000000..04bd350 --- /dev/null +++ b/lib/Rails/ActionView/Helper.php @@ -0,0 +1,123 @@ +isNamedPathMethod($method)) { + return $this->getNamedPath($method, $params); + } elseif ($helper = ViewHelpers::findHelperFor($method)) { + $helper->setView($this); + return call_user_func_array(array($helper, $method), $params); + } + + throw new Exception\BadMethodCallException( + sprintf("Called to unknown method/helper: %s", $method) + ); + } + + /** + * Returns instance of Helper\Base + */ + public function base() + { + return ViewHelpers::getBaseHelper(); + } + + public function setView(ActionView $view) + { + $this->_view = $view; + } + + public function view() + { + return $this->_view; + } + + public function urlFor($params) + { + return Rails::application()->router()->urlFor($params); + } + + public function params() + { + return Rails::application()->dispatcher()->parameters(); + } + + public function request() + { + return Rails::application()->dispatcher()->request(); + } + + public function controller() + { + return Rails::application()->controller(); + } + + public function u($str) + { + return urlencode($str); + } + + public function hexEncode($str) + { + $r = ''; + $e = strlen($str); + $c = 0; + $h = ''; + while ($c < $e) { + $h = dechex(ord(substr($str, $c++, 1))); + while (strlen($h) < 3) + $h = '%' . $h; + $r .= $h; + } + return $r; + } + + public function h($str, $flags = null, $charset = null) + { + $flags === null && $flags = ENT_COMPAT; + !$charset && $charset = Rails::application()->config()->encoding; + return htmlspecialchars($str, $flags, $charset); + } + + public function I18n() + { + return Rails::services()->get('i18n'); + } + + public function t($name) + { + return $this->I18n()->t($name); + } + + # TODO: move this method somewhere else, it doesn't belong here. + protected function parseUrlParams($url_params) + { + if ($url_params != '#' && (is_array($url_params) || (strpos($url_params, 'http') !== 0 && strpos($url_params, '/') !== 0))) { + if (!is_array($url_params)) + $url_params = array($url_params); + $url_to = Rails::application()->router()->urlFor($url_params, true); + } else { + $url_to = $url_params; + } + return $url_to; + } +} diff --git a/lib/Rails/ActionView/Helper/Base.php b/lib/Rails/ActionView/Helper/Base.php new file mode 100755 index 0000000..1276213 --- /dev/null +++ b/lib/Rails/ActionView/Helper/Base.php @@ -0,0 +1,17 @@ +assets->enabled) { + if (\Rails::config()->serve_static_assets && $options['digest']) { + if ($url = \Rails::assets()->findCompiledFile($source)) { + return $url; + } + } + + if ($file = \Rails::assets()->findFile($source)) { + return $file->url(); + } + } + + return \Rails::application()->router()->rootPath() . $source; + } else { + return $source; + } + } +} diff --git a/lib/Rails/ActionView/Helper/Methods/Date.php b/lib/Rails/ActionView/Helper/Methods/Date.php new file mode 100755 index 0000000..597b8c3 --- /dev/null +++ b/lib/Rails/ActionView/Helper/Methods/Date.php @@ -0,0 +1,62 @@ +distanceOfTimeInWords($fromTime, time(), $includeSeconds); + } + + public function distanceOfTimeInWords($fromTime, $toTime = 'now', $includeSeconds = false) + { + if (!is_int($fromTime)) { + $fromTime = strtotime($fromTime); + } + if (!is_int($toTime)) { + $toTime = strtotime($toTime); + } + + $distanceInSeconds = round($toTime - $fromTime); + + if ($distanceInSeconds < 0) { + $distanceInSeconds = round($fromTime - $toTime); + } + + $distanceInMinutes = ceil($distanceInSeconds/60); + + if ($distanceInSeconds < 30) + $t = 'less_than_a_minute'; + elseif ($distanceInSeconds < 90) + $t = 'one_minute'; + elseif ($distanceInSeconds < 2670) + $t = ['x_minutes', 't' => $distanceInMinutes]; + elseif ($distanceInSeconds < 5370) + $t = 'about_one_hour'; + elseif ($distanceInSeconds < 86370) + $t = ['about_x_hours', 't' => ceil($distanceInMinutes/60)]; + elseif ($distanceInSeconds < 151170) + $t = 'one_day'; + elseif ($distanceInSeconds < 2591970) + $t = ['x_days', 't' => ceil(($distanceInMinutes/60)/24)]; + elseif ($distanceInSeconds < 5183970) + $t = 'about_one_month'; + elseif ($distanceInSeconds < 31536059) + $t = ['x_months', 't' => ceil((($distanceInMinutes/60)/24)/31)]; + elseif ($distanceInSeconds < 39312001) + $t = 'about_one_year'; + elseif ($distanceInSeconds < 54864001) + $t = 'over_a_year'; + elseif ($distanceInSeconds < 31536001) + $t = 'almost_two_years'; + else + $t = ['about_x_years', 't' => ceil($distanceInMinutes/60/24/365)]; + + if (is_array($t)) + $t[0] = 'actionview.helper.date.' . $t[0]; + else + $t = 'actionview.helper.date.' . $t; + + return $this->t($t); + } +} diff --git a/lib/Rails/ActionView/Helper/Methods/Form.php b/lib/Rails/ActionView/Helper/Methods/Form.php new file mode 100755 index 0000000..ea3bb44 --- /dev/null +++ b/lib/Rails/ActionView/Helper/Methods/Form.php @@ -0,0 +1,224 @@ +text_field('artist', 'member_names()'); + */ +trait Form +{ + private $default_model; + + public function formField($type, $model, $property, array $attrs = array()) + { + return $this->_form_field($type, $model, $property, $attrs); + } + + public function formFor(Rails\ActiveRecord\Base $model, $attrs, \Closure $block = null) + { + if ($attrs instanceof \Closure) { + $block = $attrs; + $attrs = []; + } + + if (!isset($attrs['html'])) + $attrs['html'] = []; + + if (!isset($attrs['url'])) { + + // if (Rails::config()->ar2) { + $className = get_class($model); + if (($primaryKey = $className::table()->primaryKey()) && $model->getAttribute($primaryKey)) { + $action = 'update'; + } else { + $action = 'create'; + } + // } else { + // $action = $model->id ? 'update' : 'create'; + // } + + $attrs['url'] = ['#' . $action]; + } else { + $token = new UrlToken($attrs['url'][0]); + $action = $token->action(); + } + + $html_attrs = $attrs['html']; + + if (!isset($html_attrs['method'])) { + if ($action == 'create') + $html_attrs['method'] = 'post'; + elseif ($action == 'destroy') + $html_attrs['method'] = 'delete'; + else + $html_attrs['method'] = 'put'; + } + + # Check special attribute 'multipart'. + if (!empty($html_attrs['multipart'])) { + $html_attrs['enctype'] = 'multipart/form-data'; + unset($html_attrs['multipart']); + } + + if ($html_attrs['method'] != 'post') { + $method = $html_attrs['method']; + $html_attrs['method'] = 'post'; + } else { + $method = 'post'; + } + + $url_token = new UrlToken($attrs['url'][0]); + + if ($url_token->action() == 'create') { + $action_url = Rails::application()->router()->urlFor($url_token->token()); + } else { + list($route, $action_url) = Rails::application()->router()->url_helpers()->find_route_for_token($url_token->token(), $model); + } + + $html_attrs['action'] = $action_url; + + ob_start(); + if ($method != 'post') + echo $this->hiddenFieldTag('_method', $method, ['id' => '']); + + $block(new \Rails\ActionView\FormBuilder($this, $model)); + + return $this->contentTag('form', ob_get_clean(), $html_attrs); + } + + public function textField($model, $property, array $attrs = array()) + { + return $this->_form_field('text', $model, $property, $attrs); + } + + public function hiddenField($model, $property, array $attrs = array()) + { + return $this->_form_field('hidden', $model, $property, $attrs); + } + + public function passwordField($model, $property, array $attrs = array()) + { + return $this->_form_field('password', $model, $property, array_merge($attrs, ['value' => ''])); + } + + public function checkBox($model, $property, array $attrs = array(), $checked_value = '1', $unchecked_value = '0') + { + if ($this->_get_model_property($model, $property)) + $attrs['checked'] = 'checked'; + + $attrs['value'] = $checked_value; + + $hidden = $this->tag('input', array('type' => 'hidden', 'name' => $model.'['.$property.']', 'value' => $unchecked_value)); + + $check_box = $this->_form_field('checkbox', $model, $property, $attrs); + + return $hidden . "\n" . $check_box; + } + + public function textArea($model, $property, array $attrs = array()) + { + if (isset($attrs['size']) && is_int(strpos($attrs['size'], 'x'))) { + list($attrs['cols'], $attrs['rows']) = explode('x', $attrs['size']); + unset($attrs['size']); + } + return $this->_form_field('textarea', $model, $property, $attrs, true); + } + + public function select($model, $property, $options, array $attrs = array()) + { + if (!is_string($options)) { + $value = $this->_get_model_property($model, $property); + $options = $this->optionsForSelect($options, $value); + } + + if (isset($attrs['prompt'])) { + $options = $this->contentTag('option', $attrs['prompt'], ['value' => '', 'allow_blank_attrs' => true]) . "\n" . $options; + unset($attrs['prompt']); + } + + $attrs['value'] = $options; + + return $this->_form_field('select', $model, $property, $attrs, true); + } + + public function radioButton($model, $property, $tag_value, array $attrs = array()) + { + (string)$this->_get_model_property($model, $property) == (string)$tag_value && $attrs['checked'] = 'checked'; + $attrs['value'] = $tag_value; + return $this->_form_field('radio', $model, $property, $attrs); + } + + public function fileField($model, $property, array $attrs = array()) + { + return $this->_form_field('file', $model, $property, $attrs); + } + + public function setDefaultModel(\Rails\ActiveRecord\Base $model) + { + $this->default_model = $model; + } + + private function _form_field($field_type, $model, $property, array $attrs = array(), $content_tag = false) + { + $value = array_key_exists('value', $attrs) ? $attrs['value'] : $this->_get_model_property($model, $property); + + # Note here that the name tag attribute is forced to be underscored. + $underscoreProperty = preg_match('/[A-Z]/', $property) ? + Rails::services()->get('inflector')->underscore($property) : $property; + + $attrs['name'] = $model.'['.$underscoreProperty.']'; + + if (!isset($attrs['id'])) + $attrs['id'] = $model . '_' . $underscoreProperty; + + if ($content_tag) { + unset($attrs['value']); + return $this->contentTag($field_type, $value, $attrs); + } else { + $attrs['type'] = $field_type; + if ($value !== '') + $attrs['value'] = $value; + return $this->tag('input', $attrs); + } + } + + private function _get_model_property($model, $property) + { + $value = ''; + $mdl = false; + + if ($this->default_model) { + $mdl = $this->default_model; + } else { + $vars = Rails::application()->dispatcher()->controller()->vars(); + if (!empty($vars->$model)) { + $mdl = $vars->$model; + } + } + + if ($mdl) { + // if (!Rails::config()->ar2) { + if ($mdl->isAttribute($property)) { + $value = (string)$mdl->$property; + } elseif (($modelProps = get_class_vars(get_class($mdl))) && array_key_exists($property, $modelProps)) { + $value = (string)$mdl->$property; + } else { + # It's assumed this is a method. + $value = (string)$mdl->$property(); + } + // } else { + // /** + // * + // */ + // $value = (string)$mdl->$property(); + // } + } + + $this->default_model = null; + return $value; + } +} diff --git a/lib/Rails/ActionView/Helper/Methods/FormTag.php b/lib/Rails/ActionView/Helper/Methods/FormTag.php new file mode 100755 index 0000000..b0cca3d --- /dev/null +++ b/lib/Rails/ActionView/Helper/Methods/FormTag.php @@ -0,0 +1,219 @@ + 'ctrl', 'action' => ... ], + * it will be taken as $attrs. Therefore the action url should be passed as [ 'ctrl#action', ... ]. + */ + public function formTag($action_url = null, $attrs = [], Closure $block = null) + { + if (func_num_args() == 1 && $action_url instanceof Closure) { + $block = $action_url; + $action_url = null; + } elseif (func_num_args() == 2 && is_array($action_url) && is_string(key($action_url))) { + $block = $attrs; + $attrs = $action_url; + $action_url = null; + } elseif ($attrs instanceof Closure) { + $block = $attrs; + $attrs = []; + } + + if (!$block instanceof Closure) + throw new Exception\BadMethodCallException("One of the arguments must be a Closure"); + + if (empty($attrs['method'])) { + $attrs['method'] = 'post'; + $method = 'post'; + } elseif (($method = strtolower($attrs['method'])) != 'get') { + $attrs['method'] = 'post'; + } + + # Check special attribute 'multipart'. + if (!empty($attrs['multipart'])) { + $attrs['enctype'] = 'multipart/form-data'; + unset($attrs['multipart']); + } + + if ($action_url) + $attrs['action'] = Rails::application()->router()->urlFor($action_url); + + ob_start(); + if ($method != 'get' && $method != 'post') + echo $this->hiddenFieldTag('_method', $method, ['id' => '']); + $block(); + return $this->contentTag('form', ob_get_clean(), $attrs); + } + + public function formFieldTag($type, $name, $value = null, array $attrs = []) + { + return $this->_form_field_tag($type, $name, $value, $attrs); + } + + public function submitTag($value, array $attrs = []) + { + $attrs['type'] = 'submit'; + $attrs['value'] = $value; + !isset($attrs['name']) && $attrs['name'] = 'commit'; + return $this->tag('input', $attrs); + } + + public function textFieldTag($name, $value = null, array $attrs = []) + { + if (is_array($value)) { + $attrs = $value; + $value = null; + } + return $this->_form_field_tag('text', $name, $value, $attrs); + } + + public function hiddenFieldTag($name, $value, array $attrs = []) + { + return $this->_form_field_tag('hidden', $name, $value, $attrs); + } + + public function passwordFieldTag($name, $value = null, array $attrs = array()) + { + return $this->_form_field_tag('password', $name, $value, $attrs); + } + + public function checkBoxTag($name, $value = '1', $checked = false, array $attrs = []) + { + if ($checked) { + $attrs['checked'] = 'checked'; + } + return $this->_form_field_tag('checkbox', $name, $value, $attrs); + } + + public function textAreaTag($name, $value, array $attrs = []) + { + if (isset($attrs['size']) && is_int(strpos($attrs['size'], 'x'))) { + list($attrs['cols'], $attrs['rows']) = explode('x', $attrs['size']); + unset($attrs['size']); + } + return $this->_form_field_tag('textarea', $name, $value, $attrs, true); + } + + public function radioButtonTag($name, $value, $checked = false, array $attrs = []) + { + if ($checked) + $attrs['checked'] = 'checked'; + return $this->_form_field_tag('radio', $name, $value, $attrs); + } + + # $options may be closure, collection or an array of name => values. + public function selectTag($name, $options, array $attrs = []) + { + # This is found also in Form::select() + if (!is_string($options)) { + if (is_array($options) && ArrayTools::isIndexed($options) && count($options) == 2) + list ($options, $value) = $options; + else + $value = null; + $options = $this->optionsForSelect($options, $value); + } + + if (isset($attrs['prompt'])) { + $options = $this->contentTag('option', $attrs['prompt'], ['value' => '', 'allow_blank_attrs' => true]) . "\n" . $options; + unset($attrs['prompt']); + } + $attrs['value'] = $attrs['type'] = null; + + $select_tag = $this->_form_field_tag('select', $name, $options, $attrs, true); + return $select_tag; + } + + /** + * Note: For options to recognize the $tag_value, it must be identical to the option's value. + */ + public function optionsForSelect($options, $tag_value = null) + { + # New feature: accept anonymous functions that will return options. + if ($options instanceof Closure) { + $options = $options(); + /** + * New feature: accept collection as option in index 0, in index 1 the option name and in index 2 the value + * which are the properties of the models that will be used. + * Example: [ Category::all(), 'name', 'id' ] + * The second and third indexes can be either: + * - An attribute name + * - A public property + * - A method of the model that will return the value for the option/name + */ + } elseif (is_array($options) && count($options) == 3 && ArrayTools::isIndexed($options) && $options[0] instanceof \Rails\ActiveModel\Collection) { + list($models, $optionName, $valueName) = $options; + + $options = []; + if ($models->any()) { + $modelClass = get_class($models[0]); + + foreach ($models as $m) { + if ($modelClass::isAttribute($optionName)) { + $option = $m->getAttribute($optionName); + } elseif ($modelClass::hasPublicProperty($optionName)) { + $option = $m->$optionName; + } else { + # We assume it's a method. + $option = $m->$optionName(); + } + + if ($modelClass::isAttribute($valueName)) { + $value = $m->getAttribute($valueName); + } elseif ($modelClass::hasPublicProperty($optionName)) { + $option = $m->$optionName; + } else { + # We assume it's a method. + $value = $m->$valueName(); + } + + $options[$option] = $value; + } + } + } + $tag_value = (string)$tag_value; + + $tags = []; + foreach ($options as $name => $value) { + $value = (string)$value; + $tags[] = $this->_form_field_tag('option', null, $name, array('selected' => $value === $tag_value ? '1' : '', 'id' => '', 'value' => $value), true); + } + + return implode("\n", $tags); + } + + private function _form_field_tag($field_type, $name = null, $value, array $attrs = [], $content_tag = false) + { + !isset($attrs['id']) && $attrs['id'] = trim(str_replace(['[', ']', '()', '__'], ['_', '_', '', '_'], $name), '_'); + $name && $attrs['name'] = $name; + $value = (string)$value; + + if ($content_tag) { + return $this->contentTag($field_type, $value, $attrs); + } else { + $attrs['type'] = $field_type; + $value !== '' && $attrs['value'] = $value; + return $this->tag('input', $attrs); + } + } +} \ No newline at end of file diff --git a/lib/Rails/ActionView/Helper/Methods/Header.php b/lib/Rails/ActionView/Helper/Methods/Header.php new file mode 100755 index 0000000..70076eb --- /dev/null +++ b/lib/Rails/ActionView/Helper/Methods/Header.php @@ -0,0 +1,95 @@ +config()->assets; + + # If assets are enabled and fails to find the wanted file, normal behaviour of + # this method will be executed. + if ($assets_config->enabled) { + if (Rails::application()->config()->serve_static_assets) { + if ($fileUrl = Rails::assets()->findCompiledFile($url . '.css')) { + // $attrs['href'] = Rails::assets()->prefix() . '/' . $file; + $attrs['href'] = $fileUrl; + return $this->tag('link', $attrs); + } + } else { + $asset_file = $url . '.css'; + if ($assets_config->concat) { + if ($href = Rails::assets()->getFileUrl($asset_file )) { + $attrs['href'] = $href; + return $this->tag('link', $attrs); + } + } elseif ($paths = Rails::assets()->getFileUrls($asset_file)) { + $tags = []; + foreach ($paths as $path) { + $attrs['href'] = $path; + $tags[] = $this->tag('link', $attrs); + } + return implode("\n", $tags); + } + } + } + + $attrs['href'] = $this->_parse_url($url, '/stylesheets/', 'css'); + return $this->tag('link', $attrs); + } + + public function javascriptIncludeTag($url, array $attrs = array()) + { + empty($attrs['type']) && $attrs['type'] = 'text/javascript'; + + $assets_config = Rails::application()->config()->assets; + + # If assets are enabled and fails to find the wanted file, normal behaviour of + # this method will be executed. + if ($assets_config->enabled) { + if (Rails::application()->config()->serve_static_assets) { + if ($fileUrl = Rails::assets()->findCompiledFile($url . '.js')) { + // $attrs['src'] = Rails::assets()->prefix() . '/' . $file; + $attrs['src'] = $fileUrl; + return $this->contentTag('script', '', $attrs); + } + } else { + $asset_file = $url . '.js'; + if ($assets_config->concat) { + if ($src = Rails::assets()->getFileUrl($asset_file)) { + $attrs['src'] = $src; + return $this->contentTag('script', '', $attrs); + } + } elseif ($paths = Rails::assets()->getFileUrls($asset_file)) { + $tags = []; + + foreach ($paths as $path) { + $attrs['src'] = $path; + $tags[] = $this->contentTag('script', '', $attrs); + } + return implode("\n", $tags); + } + } + } + + $attrs['src'] = $this->_parse_url($url, '/javascripts/', 'js'); + return $this->contentTag('script', '', $attrs); + } + + private function _parse_url($url, $default_base_url, $ext) + { + $base_path = Rails::application()->router()->basePath(); + + if (strpos($url, '/') === 0) { + $url = $base_path . $url; + } elseif (strpos($url, 'http') !== 0 && strpos($url, 'www') !== 0) { + $url = $base_path . $default_base_url . $url . '.' . $ext; + } + return $url; + } +} \ No newline at end of file diff --git a/lib/Rails/ActionView/Helper/Methods/Html.php b/lib/Rails/ActionView/Helper/Methods/Html.php new file mode 100755 index 0000000..bc82690 --- /dev/null +++ b/lib/Rails/ActionView/Helper/Methods/Html.php @@ -0,0 +1,114 @@ +parseUrlParams($url_params); + $onclick = ''; + + if (isset($attrs['method'])) { + $onclick = "var f = document.createElement('form'); f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'post';"; + + if ($attrs['method'] != 'post') { + $onclick .= "var m = document.createElement('input'); m.type = 'hidden'; m.name = '_method'; m.value = '".$attrs['method']."'; f.appendChild(m);"; + } + + $onclick .= "f.action = this.href;f.submit();return false;"; + // $attrs['data-method'] = $attrs['method']; + unset($attrs['method']); + } + + if (isset($attrs['confirm'])) { + if (!$onclick) + $onclick = "if (!confirm('".$attrs['confirm']."')) return false;"; + else + $onclick = 'if (confirm(\''.$attrs['confirm'].'\')) {'.$onclick.'}; return false;'; + unset($attrs['confirm']); + } + + if ($onclick) + $attrs['onclick'] = $onclick; + + $attrs['href'] = $url_to; + + return $this->contentTag('a', $link, $attrs); + } + + public function linkToIf($condition, $link, $url_params, array $attrs = array()) + { + if ($condition) + return $this->linkTo($link, $url_params, $attrs); + else + return $link; + } + + public function autoDiscoveryLinkTag($type = 'rss', $url_params = null, array $attrs = array()) + { + if (!$url_params) { + $url_params = Rails::application()->dispatcher()->router()->route()->controller . '#' . + Rails::application()->dispatcher()->router()->route()->action; + } + $attrs['href'] = $this->parseUrlParams($url_params); + + empty($attrs['type']) && $attrs['type'] = 'application/' . strtolower($type) . '+xml'; + empty($attrs['title']) && $attrs['title'] = strtoupper($type); + + return $this->tag('link', $attrs); + } + + public function imageTag($source, array $attrs = array()) + { + $source = $this->assetPath($source); + + if (!isset($attrs['alt'])) + $attrs['alt'] = $this->humanize(pathinfo($source, PATHINFO_FILENAME)); + if (isset($attrs['size'])) + $this->_parse_size($attrs); + $attrs['src'] = $source; + return $this->tag('img', $attrs); + } + + public function mailTo($address, $name = null, array $options = array()) + { + if ($name === null) { + $name = $address; + if (isset($options['replace_at'])) + $name = str_replace('@', $options['replace_at'], $address); + if (isset($options['replace_dot'])) + $name = str_replace('.', $options['replace_dot'], $address); + } + $encode = isset($options['encode']) ? $options['encode'] : false; + + if ($encode == 'hex') { + $address = $this->hexEncode($address . '.'); + $address = str_replace(['%40', '%2e'], ['@', '.'], $address); + } + + $address_options = array('subject', 'body', 'cc', 'bcc'); + + $query = array_intersect_key($options, array_fill_keys($address_options, null)); + if ($query) + $query = '?' . http_build_query($query); + else + $query = ''; + + $address .= $query; + + $attrs = array_diff_key($options, $address_options, array_fill_keys(array('replace_at', 'replace_dot', 'encode'), null)); + $attrs['href'] = 'mailto:' . $address; + + $tag = $this->contentTag('a', $name, $attrs); + + if ($encode = 'javascript') { + $tag = "document.write('" . $tag . "');"; + return $this->javascriptTag('eval(decodeURIComponent(\'' . $this->hexEncode($tag) . '\'))'); + } else + return $tag; + } +} \ No newline at end of file diff --git a/lib/Rails/ActionView/Helper/Methods/Inflections.php b/lib/Rails/ActionView/Helper/Methods/Inflections.php new file mode 100755 index 0000000..59d1d92 --- /dev/null +++ b/lib/Rails/ActionView/Helper/Methods/Inflections.php @@ -0,0 +1,55 @@ +inflector()->pluralize($word, $locale); + } + + public function singularize($word, $locale = 'en') + { + return $this->inflector()->singularize($word, $locale); + } + + public function camelize($term, $uppercaseFirstLetter = true) + { + return $this->inflector()->camelize($term, $uppercaseFirstLetter); + } + + public function underscore($camelCasedWord) + { + return $this->inflector()->underscore($camelCasedWord); + } + + public function humanize($lowerCaseAndUnderscoredWord) + { + return $this->inflector()->humanize($lowerCaseAndUnderscoredWord); + } + + public function titleize($word) + { + return $this->inflector()->titleize($word); + } + + public function tableize($className) + { + return $this->inflector()->tableize($className); + } + + public function classify($tableName) + { + return $this->inflector()->classify($tableName); + } + + public function ordinal($number) + { + return $this->inflector()->ordinal($number); + } + + public function inflector() + { + return \Rails::services()->get('inflector'); + } +} \ No newline at end of file diff --git a/lib/Rails/ActionView/Helper/Methods/JavaScript.php b/lib/Rails/ActionView/Helper/Methods/JavaScript.php new file mode 100755 index 0000000..7629706 --- /dev/null +++ b/lib/Rails/ActionView/Helper/Methods/JavaScript.php @@ -0,0 +1,91 @@ + '\\\\', + ' '<\/', + "\r\n" => '\n', + "\n" => '\n', + "\r" => '\n', + '"' => '\\"', + "'" => "\\'" + ]; + + static private $JSON_ESCAPE = [ + '&' => '\u0026', + '>' => '\u003E', + '<' => '\u003C' + ]; + + public function linkToFunction($link, $function, array $attrs = array()) + { + $attrs['href'] = '#'; + if ($function) { + $function = trim($function); + if (strpos($function, -1) != ';') + $function .= ';'; + $function .= ' return false;'; + } else + $function = 'return false;'; + $attrs['onclick'] = $function; + + return $this->contentTag('a', $link, $attrs); + } + + public function buttonToFunction($name, $function, array $attrs = array()) + { + $attrs['href'] = '#'; + $function = trim($function); + if (strpos($function, -1) != ';') + $function .= ';'; + $function .= ' return false;'; + $attrs['onclick'] = $function; + $attrs['type'] = 'button'; + $attrs['value'] = $name; + return $this->tag('input', $attrs); + } + + public function j($str) + { + return $this->escapeJavascript($str); + } + + public function escapeJavascript($str) + { + return str_replace(array_keys(self::$JS_ESCAPE_MAP), self::$JS_ESCAPE_MAP, $str); + } + + /** + * $this->javascriptTag('alert("foo")', ['defer' => 'defer']); + * $this->javascriptTag(['defer' => 'defer'], function() { ... }); + * $this->javascriptTag(function() { ... }); + */ + public function javascriptTag($contentOrOptionWithBlock, $htmlOptions = null) + { + if ($contentOrOptionWithBlock instanceof \Closure) { + ob_start(); + $contentOrOptionWithBlock(); + $contents = ob_get_clean(); + $htmlOptions = []; + } elseif ($htmlOptions instanceof \Closure) { + ob_start(); + $htmlOptions(); + $contents = ob_get_clean(); + $htmlOptions = $contentOrOptionWithBlock; + } elseif (is_string($contentOrOptionWithBlock)) { + $contents = $contentOrOptionWithBlock; + if (!is_array($htmlOptions)) { + $htmlOptions = []; + } + } + + return $this->contentTag('script', $contents, $htmlOptions); + } + + public function jsonEscape($str) + { + return str_replace(array_keys(self::$JSON_ESCAPE), self::$JSON_ESCAPE, $str); + } +} diff --git a/lib/Rails/ActionView/Helper/Methods/Number.php b/lib/Rails/ActionView/Helper/Methods/Number.php new file mode 100755 index 0000000..0b4438f --- /dev/null +++ b/lib/Rails/ActionView/Helper/Methods/Number.php @@ -0,0 +1,26 @@ +_options($options, $escape) . ($open ? '>' : ' />'); + } + + public function contentTag($name, $content, array $options = array(), $escape = false) + { + if ($content instanceof \Closure) { + $content = $content($this->view()); + } + return $this->_content_tag_string($name, $content, $options, $escape); + } + + protected function _options(array $options = array(), $escape = false) + { + $opts = array(); + if (isset($options['allow_blank_attrs'])) { + $allow_blank_attrs = true; + unset($options['allow_blank_attrs']); + } else + $allow_blank_attrs = false; + + foreach ($options as $opt => $val) { + # "class" attribute allows array. + if ($opt == 'class' && is_array($val)) { + $val = implode(' ', \Rails\Toolbox\ArrayTools::flatten($val)); + } + + if (is_array($val)) + $val = implode(' ', $val); + + if ((string)$val === '' && !$allow_blank_attrs) + continue; + + if (is_int($opt)) + $opts[] = $val; + else { + $escape && $val = htmlentities($val); + $opts[] = $opt . '="' . $val . '"'; + } + } + return implode(' ', $opts); + } + + protected function _parse_size(&$attrs) + { + if (is_int(strpos($attrs['size'], 'x'))) { + list ($attrs['width'], $attrs['height']) = explode('x', $attrs['size']); + unset($attrs['size']); + } + } + + private function _content_tag_string($name, $content, array $options, $escape = false) + { + return '<' . $name . ' ' . $this->_options($options) . '>' . ($escape ? $this->h($content) : $content) . ''; + } +} diff --git a/lib/Rails/ActionView/Helper/Methods/Text.php b/lib/Rails/ActionView/Helper/Methods/Text.php new file mode 100755 index 0000000..0f95251 --- /dev/null +++ b/lib/Rails/ActionView/Helper/Methods/Text.php @@ -0,0 +1,33 @@ +_cycle_vars = []; + $this->_cycle_count = 0; + return; + # Reset cycle if new options were given. + } elseif ($this->_cycle_vars && $this->_cycle_vars !== $args) { + $this->_cycle_vars = []; + $this->_cycle_count = 0; + } + + if (empty($this->_cycle_vars)) + $this->_cycle_vars = $args; + if ($this->_cycle_count > count($this->_cycle_vars) - 1) + $this->_cycle_count = 0; + + $value = $this->_cycle_vars[$this->_cycle_count]; + $this->_cycle_count++; + return $value; + } +} \ No newline at end of file diff --git a/lib/Rails/ActionView/Helper/WillPaginate.delete/AbstractRenderer.php b/lib/Rails/ActionView/Helper/WillPaginate.delete/AbstractRenderer.php new file mode 100755 index 0000000..322956d --- /dev/null +++ b/lib/Rails/ActionView/Helper/WillPaginate.delete/AbstractRenderer.php @@ -0,0 +1,168 @@ +collection = $collection; + $this->helper = $helper; + $this->options = array_merge($this->defaultOptions(), $options); + } + + public function toHtml() + { + $html = implode(array_map(function($item){ + if (is_int($item)) + return $this->pageNumber($item); + else + return $this->$item(); + }, $this->pagination()), $this->options['link_separator']); + + return $this->options['container'] ? $this->htmlContainer($html) : $html; + } + + protected function defaultOptions() + { + return [ + 'previous_label' => '← ' . $this->helper->t('actionview.helper.will_paginate.previous'), + 'next_label' => $this->helper->t('actionview.helper.will_paginate.next') . ' →', + 'container' => true, + 'link_separator' => ' ' + ]; + } + + protected function pageNumber($page) + { + if ($page != $this->collection->currentPage()) + return $this->link($page, $page, ['rel' => $this->relValue($page)]); + else + return $this->tag('span', $page, ['class' => 'current']); + } + + protected function gap() + { + return ''; + } + + protected function previousPage() + { + $num = $this->collection->currentPage() > 1 ? + $this->collection->currentPage() - 1 : false; + return $this->previousOrNextPage($num, $this->options['previous_label'], 'previousPage'); + } + + protected function nextPage() + { + $num = $this->collection->currentPage() < $this->collection->totalPages() ? + $this->collection->currentPage() + 1 : false; + return $this->previousOrNextPage($num, $this->options['next_label'], 'nextPage'); + } + + protected function previousOrNextPage($page, $text, $classname) + { + if ($page) + return $this->link($text, $page, ['class' => $classname]); + else + return $this->tag('span', $text, ['class' => $classname . ' disabled']); + } + + protected function htmlContainer($html) + { + return $this->tag('div', $html, $this->containerAttributes()); + } + + protected function containerAttributes() + { + return ['class' => 'pagination']; + } + + protected function relValue($page) + { + if ($this->collection->currentPage() - 1 == $page) + return 'prev' . ($page == 1 ? ' start' : ''); + elseif ($this->collection->currentPage() + 1 == $page) + return 'next'; + elseif ($page == 1) + return 'start'; + } + + protected function pagination() + { + $pages = $this->collection->totalPages(); + $page = $this->collection->currentPage(); + $pagination = []; + + $pagination[] = 'previousPage'; + $pagination[] = 1; + + if ($pages < 10){ + for ($i = 2; $i <= $pages; $i++){ + $pagination[] = $i; + } + } elseif ($page > ($pages - 4)) { + $pagination[] = 'gap'; + for ($i = ($pages - 4); $i < ($pages); $i++) { + $pagination[] = $i; + } + } elseif ($page > 4) { + $pagination[] = 'gap'; + for ($i = ($page - 1); $i <= ($page + 2); $i++) { + $pagination[] = $i; + } + $pagination[] = 'gap'; + } else { + if ($page >= 3){ + for ($i = 2; $i <= $page+2; $i++) { + $pagination[] = $i; + } + } else { + for ($i = 2; $i <= 5; $i++) { + $pagination[] = $i; + } + } + $pagination[] = 'gap'; + } + + if ($pages >= 10) { + if ($pages == $page) + $pagination[] = $i; + else + $pagination[] = $pages; + } + + $pagination[] = 'nextPage'; + + return $pagination; + } + + protected function link($text, $page, array $attrs = []) + { + return $this->helper->linkTo($text, array_merge(['#index'], $this->params()->query_parameters(), ['page' => $page]), $attrs); + } + + protected function tag($type, $content, array $attrs = []) + { + return $this->helper->contentTag($type, $content, $attrs); + } + + protected function params() + { + return Rails::application()->dispatcher()->parameters(); + } +} \ No newline at end of file diff --git a/lib/Rails/ActionView/Helper/WillPaginate.delete/BootstrapRenderer.php b/lib/Rails/ActionView/Helper/WillPaginate.delete/BootstrapRenderer.php new file mode 100755 index 0000000..66af10e --- /dev/null +++ b/lib/Rails/ActionView/Helper/WillPaginate.delete/BootstrapRenderer.php @@ -0,0 +1,52 @@ +pageNumber($item); + else + return $this->$item(); + }, $this->pagination()), $this->options['link_separator']); + + return $this->htmlContainer($this->tag('ul', $html)); + } + + protected function pageNumber($page) + { + if ($page != $this->collection->currentPage()) + return $this->tag('li', $this->link($page, $page, ['rel' => $this->relValue($page)])); + else + return $this->tag('li', $this->tag('span', $page), ['class' => 'current']); + } + + protected function gap() + { + return $this->tag('li', $this->link('…', "#"), ['class' => 'disabled']); + } + + protected function previousPage() + { + $num = $this->collection->currentPage() > 1 ? + $this->collection->currentPage() - 1 : false; + return $this->previousOrNextPage($num, $this->options['previous_label'], 'prev'); + } + + protected function nextPage() + { + $num = $this->collection->currentPage() < $this->collection->totalPages() ? + $this->collection->currentPage() + 1 : false; + return $this->previousOrNextPage($num, $this->options['next_label'], 'next'); + } + + protected function previousOrNextPage($page, $text, $classname) + { + if ($page) + return $this->tag('li', $this->link($text, $page), ['class' => $classname]); + else + return $this->tag('li', $this->tag('span', $text), ['class' => $classname . ' disabled']); + } +} \ No newline at end of file diff --git a/lib/Rails/ActionView/Helper/WillPaginate.delete/LegacyRenderer.php b/lib/Rails/ActionView/Helper/WillPaginate.delete/LegacyRenderer.php new file mode 100755 index 0000000..8c843c0 --- /dev/null +++ b/lib/Rails/ActionView/Helper/WillPaginate.delete/LegacyRenderer.php @@ -0,0 +1,8 @@ +template_filename = $layoutName; + + $this->filename = $this->resolve_layout_file($layoutName); + + if (isset($params['contents'])) { + $this->_buffer = $params['contents']; + } + + $locals && $this->setLocals($locals); + } + + public function renderContent() + { + ob_start(); + require $this->filename; + $this->_buffer = ob_get_clean(); + } +} \ No newline at end of file diff --git a/lib/Rails/ActionView/Partial.php b/lib/Rails/ActionView/Partial.php new file mode 100755 index 0000000..98e11e3 --- /dev/null +++ b/lib/Rails/ActionView/Partial.php @@ -0,0 +1,17 @@ +_init_render(); + return ob_get_clean(); + } + + public function t($name, array $params = []) + { + return parent::t($name, $params); + } +} \ No newline at end of file diff --git a/lib/Rails/ActionView/Template.php b/lib/Rails/ActionView/Template.php new file mode 100755 index 0000000..d8697bc --- /dev/null +++ b/lib/Rails/ActionView/Template.php @@ -0,0 +1,288 @@ + $render_params]; + } + + $this->type = key($render_params); + + switch ($this->type) { + case 'file': + $this->template_file = array_shift($render_params); + break; + + /** + * Allows to receive the contents for this template, i.e. + * no processing is needed. It will just be added to the layout, if any. + */ + case 'contents': + $this->contents = array_shift($render_params); + break; + + /** + * Contents will be evalued. + */ + case 'inline': + $this->inline_code = array_shift($render_params); + break; + + case 'lambda': + $this->lambda = array_shift($render_params); + break; + } + $this->params = $params; + + if (isset($params['locals'])) { + $locals = $params['locals']; + unset($params['locals']); + } + + $locals && $this->setLocals($locals); + } + + public function renderContent() + { + Rails\ActionView\ViewHelpers::load(); + $this->_set_initial_ob_level(); + + if (!$this->_init_rendered) { + ob_start(); + $this->_init_rendered = true; + $this->_init_render(); + $this->_buffer = ob_get_clean(); + + if (!$this->_validate_ob_level()) { + $status = ob_get_status(); + throw new Exception\OutputLeakedException( + sprintf('Buffer level: %s; File: %s
Topmost buffer\'s contents:
%s', + $status['level'], substr($this->_filename, strlen(Rails::root()) + 1), htmlentities(ob_get_clean())) + ); + } + } + return $this; + } + + protected function render(array $params) + { + $type = key($params); + switch ($type) { + case 'template': + $file = array_shift($params); + $template = new self(['file' => $file]); + $template->renderContent(); + return $template->content(); + break; + } + } + + public function content($name = null) + { + if (!func_num_args()) + return $this->_buffer; + else + return parent::content($name); + } + + public function contents() + { + return $this->content(); + } + + /** + * Finally prints all the view. + */ + public function get_buffer_and_clean() + { + $buffer = $this->_buffer; + $this->_buffer = null; + return $buffer; + } + + public function t($name, array $params = []) + { + if (is_array($name)) { + $params = $name; + $name = current($params); + } + + if (strpos($name, '.') === 0) { + if (is_int(strpos($this->template_filename, Rails::config()->paths->views->toString()))) { + $parts = array(); + $path = substr( + $this->template_filename, strlen(Rails::config()->paths->views) + 1, + strlen(pathinfo($this->template_filename, PATHINFO_BASENAME)) * -1 + ) + . pathinfo($this->template_filename, PATHINFO_FILENAME); + + foreach (explode('/', $path) as $part) { + $parts[] = ltrim($part, '_'); + } + $name = implode('.', $parts) . $name; + } + } + + return parent::t($name, $params); + } + + protected function _init_render() + { + $layout_wrap = !empty($this->params['layout']); + + if ($layout_wrap) { + $layout_file = $this->resolve_layout_file($this->params['layout']); + + if (!is_file($layout_file)) + throw new Exception\LayoutMissingException( + sprintf("Missing layout '%s' [ file => %s ]", + $this->params['layout'], $layout_file) + ); + ob_start(); + } + + switch ($this->type) { + case 'file': + $this->build_template_filename(); + + if (!is_file($this->template_filename)) { + throw new Exception\TemplateMissingException( + sprintf("Missing template file %s", $this->template_filename) + ); + } + + require $this->template_filename; + break; + + case 'contents': + echo $this->contents; + break; + + case 'inline': + eval('?>' . $this->inline_code . 'lambda; + $this->lambda = null; + $lambda = $lambda->bindTo($this); + $lambda(); + break; + } + + if ($layout_wrap) { + $this->_buffer = ob_get_clean(); + $layout = new Layout($layout_file, ['contents' => $this->_buffer], $this->locals); + $layout->renderContent(); + echo $layout->content(); + // require $layout_file; + } + } + + private function _set_initial_ob_level() + { + $status = ob_get_status(); + $this->_initial_ob_level = $this->_get_ob_level(); + } + + private function _validate_ob_level() + { + $status = ob_get_status(); + return $this->_initial_ob_level == $this->_get_ob_level(); + } + + protected function resolve_layout_file($layout) + { + if (strpos($layout, '/') === 0 || preg_match('/^[A-Za-z]:(?:\\\|\/)/', $layout)) + return $layout; + else { + if (is_int(strpos($layout, '.'))) + return Rails::config()->paths->layouts . '/' . $layout; + else + return Rails::config()->paths->layouts . '/' . $layout . '.php'; + } + } + + private function build_template_filename() + { + $template = $this->template_file; + if (strpos($template, '/') === 0 || preg_match('/^[A-Za-z]:(?:\\\|\/)/', $template)) + $this->template_filename = $template; + else { + if (is_int(strpos($template, '.'))) { + $this->template_filename = Rails::config()->paths->views . '/' . $template; + } else { + $this->template_filename = Rails::config()->paths->views . '/' . $template . '.php'; + } + } + } + + private function _get_ob_level() + { + $status = ob_get_status(); + return !empty($status['level']) ? $status['level'] : 0; + } +} \ No newline at end of file diff --git a/lib/Rails/ActionView/Template/Exception/ExceptionInterface.php b/lib/Rails/ActionView/Template/Exception/ExceptionInterface.php new file mode 100755 index 0000000..787a9bb --- /dev/null +++ b/lib/Rails/ActionView/Template/Exception/ExceptionInterface.php @@ -0,0 +1,6 @@ + $helper) { + if (method_exists($helper, $method)) { + self::$registry[$method] = $helperName; + return $helper; + } + } + return false; + } + + static public function getBaseHelper() + { + return self::getHelper(self::BASE_HELPER_NAME); + } + + static public function getHelper($name) + { + return self::$helpers[$name]; + } + + /** + * Adds class names to the helper queues. These classes will be instantiated + * in includeHelpers(). + * + * @var name string helper name + * @see load() + */ + static public function addHelper($className) + { + self::$queue[] = $className; + } + + static public function addHelpers(array $classNames) + { + self::$queue = array_merge(self::$queue, $className); + } + + /** + * For application helpers. Class names passed will be appended with "Helper". + */ + static public function addAppHelpers(array $helpers) + { + self::$queue = array_merge(self::$queue, array_map(function($c) { return $c . 'Helper'; }, $helpers)); + } + + /** + * Actually include the helpers files. + * + * Application and current controller's helper are added here, + * to make sure they're top on the list. + */ + static public function load() + { + if (!self::$helpersLoaded) { + if (($router = Rails::application()->dispatcher()->router()) && ($route = $router->route())) { + $controllerHelper = Rails::services()->get('inflector')->camelize($route->controller()) . 'Helper'; + array_unshift(self::$queue, $controllerHelper); + } + $appHelper = 'ApplicationHelper'; + array_unshift(self::$queue, $appHelper); + + foreach (array_unique(self::$queue) as $name) { + try { + Rails::loader()->loadClass($name); + self::$helpers[$name] = new $name(); + } catch (Rails\Loader\Exception\ExceptionInterface $e) { + } + } + + # Add base helper + self::$helpers[self::BASE_HELPER_NAME] = new Helper\Base(); + + self::$helpersLoaded = true; + } + } +} diff --git a/lib/Rails/ActionView/Xml.php b/lib/Rails/ActionView/Xml.php new file mode 100755 index 0000000..bde9bdc --- /dev/null +++ b/lib/Rails/ActionView/Xml.php @@ -0,0 +1,69 @@ +_buffer .= ''."\n"; + } + + /** + * $content could be passed as second argument. + */ + public function create($root, $attrs, $content = null) + { + $this->_buffer .= '<'.$root; + + if (!is_array($attrs)) { + $content = $attrs; + $attrs = []; + } + + if ($attrs) { + $attrs_str = []; + foreach ($attrs as $name => $val) + $attrs_str[] = $name . '="'.htmlspecialchars($val).'"'; + $this->_buffer .= ' ' . implode(' ', $attrs_str); + } + + if (!$content) { + $this->_buffer .= ' />'; + } else { + $this->_buffer .= ">\n"; + + if (is_string($content)) + $this->_buffer .= $content; + elseif ($content instanceof Closure) + $this->_buffer .= $content(); + else + throw new Exception\InvalidArgumentError( + sprintf('Expecting Closure or string as third argument, %s passed.', gettype($content)) + ); + + $this->_buffer .= ''; + } + } + + public function build($el, array $params = []) + { + $this->_buffer .= (new \Rails\Xml\Xml($el, $params))->output() . "\n"; + } + + public function output() + { + !$this->_buffer && $this->create(); + return $this->_buffer; + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveModel/Collection.php b/lib/Rails/ActiveModel/Collection.php new file mode 100755 index 0000000..17de1a6 --- /dev/null +++ b/lib/Rails/ActiveModel/Collection.php @@ -0,0 +1,366 @@ +members[] = $value; + } else { + $this->members[$offset] = $value; + } + } + + public function offsetExists($offset) + { + return isset($this->members[$offset]); + } + + public function offsetUnset($offset) + { + unset($this->members[$offset]); + } + + public function offsetGet($offset) + { + return isset($this->members[$offset]) ? $this->members[$offset] : null; + } + /* } Iterator {*/ + protected $position = 0; + + public function rewind() + { + reset($this->members); + $this->position = key($this->members); + } + + public function current() + { + return $this->members[$this->position]; + } + + public function key() + { + return key($this->members); + } + + public function next() + { + next($this->members); + $this->position = key($this->members); + } + + public function valid() + { + return array_key_exists($this->position, $this->members); + } + /* } */ + + public function __construct(array $members = array()) + { + $this->members = $members; + } + + public function merge() + { + foreach (func_get_args() as $coll) { + if ($coll instanceof self) + $coll = $coll->members(); + $this->members = array_merge($this->members, $coll); + } + return $this; + } + + public function members() + { + return $this->members; + } + + # Another way to get members. + public function toArray() + { + return $this->members; + } + + /** + * Each (experimental) + * + * If string is passed, it'll be taken as method name to be called. + * Eg. $posts->each('destroy'); - All posts will be destroyed. + * In this case, $params for the method may be passed. + * + * A Closure may also be passed. + */ + public function each($function, array $params = array()) + { + if (is_string($function)) { + foreach ($this->members() as $m) { + call_user_func_array(array($m, $function), $params); + } + } elseif ($function instanceof Closure) { + foreach ($this->members() as $idx => $m) { + $function($m, $idx); + } + } else { + throw new Exception\InvalidArgumentException( + sprintf('Argument must be an either a string or a Closure, %s passed.', gettype($model)) + ); + } + } + + public function reduce($var, Closure $block) + { + foreach ($this->members() as $m) { + $var = $block($var, $m); + } + return $var; + } + + public function sort(Closure $criteria) + { + usort($this->members, $criteria); + $this->rewind(); + return $this; + } + + public function unshift($model) + { + if ($model instanceof Base) + $model = array($model); + elseif (!$model instanceof self) { + throw new Exception\InvalidArgumentException( + sprintf('Argument must be an instance of either ActiveRecord\Base or ActiveRecord\Collection, %s passed.', gettype($model)) + ); + } + + foreach ($model as $m) + array_unshift($this->members, $m); + + return $this; + } + + /** + * Searches objects for a property with a value and returns object. + */ + public function search($prop, $value) + { + foreach ($this->members() as $obj) { + if ($obj->$prop == $value) + return $obj; + } + return false; + } + + # Returns a Collection with the models that matches the options. + # Eg: $posts->select(array('is_active' => true, 'user_id' => 4)); + # If Closure passed as $opts, the model that returns == true on the function + # will be added. + public function select($opts) + { + $objs = array(); + + if (is_array($opts)) { + foreach ($this as $obj) { + foreach ($opts as $prop => $cond) { + if (!$obj->$prop || $obj->$prop != $cond) + continue; + $objs[] = $obj; + } + } + } elseif ($opts instanceof Closure) { + foreach ($this->members() as $obj) { + $opts($obj) && $objs[] = $obj; + } + } + + return new self($objs); + } + + /** + * Removes members according to attributes. + */ + public function remove($attrs) + { + !is_array($attrs) && $attrs = array('id' => $attrs); + + foreach ($this->members() as $k => $m) { + foreach ($attrs as $a => $v) { + if ($m->getAttribute($a) != $v) + continue 2; + } + unset($this->members[$k]); + } + $this->members = array_values($this->members); + return $this; + } + + public function max($criteria) + { + if (!$this->members) + return false; + + $current = key($this->members); + if (count($this->members) < 2) + return $this->members[$current]; + + $max = $this->members[$current]; + + if ($criteria instanceof Closure) { + $params = $this; + foreach($params as $current) { + if (!$next = next($params)) + break; + $max = $criteria($max, $next); + } + } else { + + } + return $max; + } + + # Deprecated in favor of none. + public function blank() + { + return empty($this->members); + } + + public function none() + { + return empty($this->members); + } + + public function any() + { + return (bool)$this->members; + } + + /** + * TODO: xml shouldn't be created here. + */ + public function toXml() + { + if ($this->blank()) + return; + + $t = get_class($this->current()); + $t = $t::t(); + + $xml = ''; + $xml .= '<' . $t . '>'; + + foreach ($this->members() as $obj) { + $xml .= $obj->toXml(array('skip_instruct' => true)); + } + + $xml .= ''; + + return $xml; + } + + public function asJson() + { + $json = []; + foreach ($this->members as $member) + $json[] = $member->asJson(); + return $json; + } + + public function toJson() + { + return json_encode($this->asJson()); + } + + /** + * Returns an array of the attributes in the models. + * $attrs could be a string of a single attribute we want, and + * an indexed array will be returned. + * If $attrs is an array of many attributes, an associative array will be returned. + * + * $collection->attributes(['id', 'createdAt']); + * No -> $collection->attributes('id', 'name'); + */ + public function getAttributes($attrs) + { + $models_attrs = array(); + + if (is_string($attrs)) { + foreach ($this as $m) { + // if (Rails::config()->ar2) { + // $models_attrs[] = $m->$attrs(); + // } else { + $models_attrs[] = $m->$attrs; + // } + } + } else { + foreach ($this->members() as $m) { + $model_attrs = []; + foreach ($attrs as $attr) { + // if (!Rails::config()->ar2) { + $model_attrs[$attr] = $m->$attr; + // } else { + // $model_attrs[$attr] = $m->$attr(); + // } + } + $models_attrs[] = $model_attrs; + } + } + + return $models_attrs; + } + + public function size() + { + return count($this->members); + } + + # Removes dupe models based on id or other attribute. + public function unique($attr = 'id') + { + $checked = array(); + foreach ($this->members() as $k => $obj) { + if (in_array($obj->$attr, $checked)) + unset($this->members[$k]); + else + $checked[] = $obj->$attr; + } + return $this; + } + + # array_slices the collection. + public function slice($offset, $length = null) + { + $clone = clone $this; + $clone->members = array_slice($clone->members, $offset, $length); + return $clone; + } + + public function deleteIf(Closure $conditions) + { + $deleted = false; + foreach ($this->members() as $k => $m) { + if ($conditions($m)) { + unset($this[$k]); + $deleted = true; + } + } + if ($deleted) + $this->members = array_values($this->members); + } + + public function replace($replacement) + { + if ($replacement instanceof self) + $this->members = $replacement->members(); + elseif (is_array($replacement)) + $this->members = $replacement; + else + throw new Exception\InvalidArgumentException(sprintf("%s expects a %s or an array, %s passed", __METHOD__, __CLASS__, gettype($replacement))); + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveModel/Errors.php b/lib/Rails/ActiveModel/Errors.php new file mode 100755 index 0000000..e7e9dd6 --- /dev/null +++ b/lib/Rails/ActiveModel/Errors.php @@ -0,0 +1,107 @@ +errors[$attribute])) + $this->errors[$attribute] = array(); + + $this->errors[$attribute][] = $msg; + } + + public function addToBase($msg) + { + $this->base($msg); + } + + public function base($msg) + { + $this->add(self::BASE_ERRORS_INDEX, $msg); + } + + public function on($attribute) + { + if (!isset($this->errors[$attribute])) + return null; + elseif (count($this->errors[$attribute]) == 1) + return current($this->errors[$attribute]); + else + return $this->errors[$attribute]; + } + + public function onBase() + { + return $this->on(self::BASE_ERRORS_INDEX); + } + + # $glue is a string that, if present, will be used to + # return the messages imploded. + public function fullMessages($glue = null) + { + $fullMessages = array(); + + foreach ($this->errors as $attr => $errors) { + foreach ($errors as $msg) { + if ($attr == self::BASE_ERRORS_INDEX) + $fullMessages[] = $msg; + else + $fullMessages[] = $this->_propper_attr($attr) . ' ' . $msg; + } + } + + if ($glue !== null) + return implode($glue, $fullMessages); + else + return $fullMessages; + } + + public function invalid($attribute) + { + return isset($this->errors[$attribute]); + } + + # Deprecated in favor of none(). + public function blank() + { + return !(bool)$this->errors; + } + + public function none() + { + return !(bool)$this->errors; + } + + public function any() + { + return !$this->blank(); + } + + public function all() + { + return $this->errors; + } + + public function count() + { + $i = 0; + foreach ($this->errors as $errors) { + $i += count($errors); + } + return $i; + } + + private function _propper_attr($attr) + { + $attr = ucfirst(strtolower($attr)); + if (is_int(strpos($attr, '_'))) { + $attr = str_replace('_', ' ', $attr); + } + return $attr; + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveModel/Exception/ExceptionInterface.php b/lib/Rails/ActiveModel/Exception/ExceptionInterface.php new file mode 100755 index 0000000..9f6197c --- /dev/null +++ b/lib/Rails/ActiveModel/Exception/ExceptionInterface.php @@ -0,0 +1,6 @@ + ActiveRecord\Connection + */ + $_connections = []; + + /** + * Name of the active connection. + */ + static private $activeConnectionName; + + /** + * If an error ocurred when calling execute_sql(), + * it will be stored here. + */ + static private $_last_error; + + static public function setLastError(array $error, $connection_name = null) + { + self::$_last_error = $error; + } + + /** + * Adds a connection configuration to the list of available connections. + */ + static public function addConnection($config, $name) + { + self::$_connection_data[$name] = $config; + } + + /** + * Sets the default active connection. + */ + static public function setConnection($name) + { + self::create_connection($name); + self::$activeConnectionName = $name; + return true; + } + + /** + * Stablishes a connection from the available connections list. + */ + static protected function create_connection($name) + { + # If the connection is already created, return. + if (isset(self::$_connections[$name])) + return; + elseif (self::connectionExists($name)) + self::$_connections[$name] = new Connection(self::$_connection_data[$name], $name); + else + throw new Exception\RuntimeException( + sprintf("Connection '%s' does not exist", $name) + ); + } + + static public function connectionExists($name) + { + return isset(self::$_connection_data[$name]); + } + + static public function set_environment_connection($environment) + { + if (self::connectionExists($environment)) + self::setConnection($environment); + elseif (self::connectionExists('default')) + self::setConnection('default'); + } + + /** + * Returns connection. By default, returns the + * currenct active one, or the one matching the $name. + * + * @return ActiveRecord\Connection + */ + static public function connection($name = null) + { + if ($name === null) { + $name = self::$activeConnectionName; + if (!$name) + throw new Exception\RuntimeException("No database connection is active"); + } else { + self::create_connection($name); + } + + return self::$_connections[$name]; + } + + /** + * Returns active connection's name. + */ + static public function activeConnectionName() + { + return self::$activeConnectionName; + } + + /** + * Returns active connection's data. + */ + static public function activeConnectionData() + { + if (!self::$activeConnectionName) + throw new Exception\RuntimeException("No database connection is active"); + return array_merge(self::$_connection_data[self::$activeConnectionName], ['name' => self::$activeConnectionName]); + } + + /** + * Used by the system when creating schema files. + */ + static public function connections() + { + return self::$_connection_data; + } + + static public function lastError() + { + return self::$_last_error; + } + + static private function _include_additional_connection($name) + { + $config = Rails::application()->config()->active_record; + if (!empty($config['additional_connections']) && !empty($config['additional_connections'][$name])) { + self::addConnection($config['additional_connections'][$name], $name); + return true; + } + return false; + } + + /** + * This could be somewhere else. + */ + static public function proper_adapter_name($adapter_name) + { + $adapter_name = strtolower($adapter_name); + + switch ($adapter_name) { + case 'mysql': + return 'MySql'; + break; + + case 'sqlite': + return 'Sqlite'; + break; + + default: + return $adapter_name; + break; + } + } + + static public function restore_connection() + { + if (self::$_prev_connection) { + self::setConnection(self::$_prev_connection); + self::$_prev_connection = null; + } + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveRecord/Adapter/AbstractQueryBuilder.php b/lib/Rails/ActiveRecord/Adapter/AbstractQueryBuilder.php new file mode 100755 index 0000000..42ca093 --- /dev/null +++ b/lib/Rails/ActiveRecord/Adapter/AbstractQueryBuilder.php @@ -0,0 +1,62 @@ +connection = $connection; + } + + public function executeSql() + { + $this->_stmt = $this->connection->executeSql($this->_params); + if ($this->will_paginate) + $this->calculate_found_rows(); + } + + abstract protected function calculate_found_rows(); + + abstract protected function _build_sql(); + + public function build_sql(AbstractRelation $query) + { + $complete_sql = $query->complete_sql(); + $this->query = $query; + + if ($complete_sql) { + list ($sql, $params) = $complete_sql; + array_unshift($params, $sql); + $this->_params = $params; + $this->will_paginate = $query->will_paginate(); + } else { + $this->will_paginate = $query->will_paginate(); + $this->_build_sql(); + } + } + + public function stmt() + { + return $this->_stmt; + } + + public function row_count() + { + return $this->_row_count; + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveRecord/Adapter/AbstractTable.php b/lib/Rails/ActiveRecord/Adapter/AbstractTable.php new file mode 100755 index 0000000..85e4ed7 --- /dev/null +++ b/lib/Rails/ActiveRecord/Adapter/AbstractTable.php @@ -0,0 +1,12 @@ +connection->query("SELECT FOUND_ROWS()"); + $rows = array_shift($rows); + $rows = array_shift($rows); + $this->_row_count = (int)$rows; + } + + protected function _build_sql() + { + $query = $this->query; + + $params = []; + + $sql = 'SELECT'; + + if ($this->will_paginate) + $sql .= ' SQL_CALC_FOUND_ROWS'; + + if ($query->distinct) + $sql .= ' DISTINCT'; + + $sql .= ' ' . ($query->select ? implode(', ', $query->select) : '`' . $query->from . '`.*'); + $sql .= " FROM `" . $query->from . "`"; + $sql .= ' ' . implode(' ', $query->joins); + + if ($where = $this->_build_where_clause($query)) { + $sql .= " WHERE " . $where[0]; + $params = $where[1]; + unset($where); + } + + if ($query->group) + $sql .= ' GROUP BY ' . implode(', ', $query->group); + + if ($query->order) + $sql .= ' ORDER BY ' . implode(', ', $query->order); + + if ($query->having) { + $sql .= ' HAVING ' . implode(' ', $query->having); + $params = array_merge($params, $query->having_params); + } + + if ($query->offset && $query->limit) + $sql .= ' LIMIT ' . $query->offset . ', ' . $query->limit; + elseif ($query->limit) + $sql .= ' LIMIT ' . $query->limit; + + array_unshift($params, $sql); + $this->_params = $params; + } + + private function _build_where_clause($query) + { + if (!$query->where) + return; + // if (end($query->_where) == 'name in (?)') + // vpe($query->where_params); + $where = $where_params = []; + $param_count = 0; + + foreach ($query->where as $condition) { + # Case: ["foo" => $foo, "bar_baz" => $bar]; + if (is_array($condition)) { + foreach ($condition as $column => $value) { + $where[] = $column . ' = ?'; + $where_params[] = $value; + } + } else { + if ($count = substr_count($condition, '?')) { + foreach (range(0, $count - 1) as $i) { + if (!array_key_exists($param_count, $query->where_params)) + throw new Exception\RuntimeException(sprintf("Value for question mark placeholder for WHERE clause section wasn't found: %s", $condition)); + + if (is_array($query->where_params[$param_count])) { + $condition = preg_replace('/\?/', implode(', ', array_fill(0, count($query->where_params[$param_count]), '?')), $condition, 1); + $where_params = array_merge($where_params, $query->where_params[$param_count]); + } else { + $where_params[] = $query->where_params[$param_count]; + } + $param_count++; + } + } elseif ($count = substr_count($condition, ':')) { + $where_params = $query->where_params; + } + + $where[] = $condition; + } + } + + return [implode(' AND ', $where), $where_params]; + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveRecord/Adapter/MySql/Table.php b/lib/Rails/ActiveRecord/Adapter/MySql/Table.php new file mode 100755 index 0000000..b94b198 --- /dev/null +++ b/lib/Rails/ActiveRecord/Adapter/MySql/Table.php @@ -0,0 +1,55 @@ +executeSql("DESCRIBE `".$table_name."`"); + if (!$rows = $stmt->fetchAll()) { + throw new Exception\RuntimeException( + sprintf("Couldn't DESCRIBE %s:\n%s", $table_name, var_export($stmt->errorInfo(), true)) + ); + } + + $table_data = $table_indexes = $pri = $uni = []; + + foreach ($rows as $row) { + $data = ['type' => $row['Type']]; + + if (strpos($row['Type'], 'enum') === 0) { + $enum_values = []; + foreach (explode(',', substr($row['Type'], 5, -1)) as $opt) + $enum_values[] = substr($opt, 1, -1); + $data['enum_values'] = $enum_values; + } + + $table_data[$row['Field']] = $data; + } + + $stmt = $connection->executeSql("SHOW INDEX FROM `".$table_name."`"); + $idxs = $stmt->fetchAll(); + + if ($idxs) { + foreach ($idxs as $idx) { + if ($idx['Key_name'] == 'PRIMARY') { + $pri[] = $idx['Column_name']; + } elseif ($idx['Non_unique'] === '0') { + $uni[] = $idx['Column_name']; + } + } + } + + if ($pri) + $table_indexes['pri'] = $pri; + elseif ($uni) + $table_indexes['uni'] = $uni; + + return [$table_data, $table_indexes]; + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveRecord/Adapter/Sqlite/QueryBuilder.php b/lib/Rails/ActiveRecord/Adapter/Sqlite/QueryBuilder.php new file mode 100755 index 0000000..f132968 --- /dev/null +++ b/lib/Rails/ActiveRecord/Adapter/Sqlite/QueryBuilder.php @@ -0,0 +1,99 @@ +query->except('limit')->except('offset')->except('select')->select('COUNT(*) as count_all'); + $this->_build_sql(); + + if ($stmt = $this->connection->executeSql($this->_params)) { + $rows = $stmt->fetchAll(); + if (isset($rows[0]['count_all'])) + return $rows[0]['count_all']; + } + } + + protected function _build_sql() + { + $query = $this->query; + + $params = []; + + $sql = 'SELECT'; + + if ($query->distinct) + $sql .= ' DISTINCT'; + + $sql .= ' ' . ($query->select ? implode(', ', $query->select) : '`' . $query->from . '`.*'); + $sql .= " FROM `" . $query->from . "`"; + $sql .= ' ' . implode(' ', $query->joins); + + if ($where = $this->_build_where_clause($query)) { + $sql .= " WHERE " . $where[0]; + $params = $where[1]; + unset($where); + } + + if ($query->group) + $sql .= ' GROUP BY ' . implode(', ', $query->group); + + if ($query->order) + $sql .= ' ORDER BY ' . implode(', ', $query->order); + + if ($query->having) { + $sql .= ' HAVING ' . implode(' ', $query->having); + $params = array_merge($params, $query->having_params); + } + + if ($query->offset && $query->limit) + $sql .= ' LIMIT ' . $query->offset . ', ' . $query->limit; + elseif ($query->limit) + $sql .= ' LIMIT ' . $query->limit; + + array_unshift($params, $sql); + $this->_params = $params; + } + + private function _build_where_clause($query) { + if (!$query->where) + return; + + $where = $where_params = []; + $param_count = 0; + + foreach ($query->where as $condition) { + # Case: ["foo" => $foo, "bar" => $bar]; + if (is_array($condition)) { + foreach ($condition as $column => $value) { + $where[] = '`' . $column . '` = ?'; + $where_params[] = $value; + } + } else { + if ($count = substr_count($condition, '?')) { + foreach (range(0, $count - 1) as $i) { + if (!isset($query->where_params[$param_count])) + throw new Exception\RuntimeException(sprintf("Value for question mark placeholder for WHERE clause part wasn't found (%s)", $condition)); + + if (is_array($query->where_params[$param_count])) { + $condition = preg_replace('/\?/', implode(', ', array_fill(0, count($query->where_params[$param_count]), '?')), $condition, 1); + } + + $where_params[] = $query->where_params[$param_count]; + $param_count++; + } + } elseif ($count = substr_count($condition, ':')) { + $where_params = $query->where_params; + } + + $where[] = $condition; + } + } + + return [implode(' AND ', $where), $where_params]; + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveRecord/Adapter/Sqlite/Table.php b/lib/Rails/ActiveRecord/Adapter/Sqlite/Table.php new file mode 100755 index 0000000..a151d53 --- /dev/null +++ b/lib/Rails/ActiveRecord/Adapter/Sqlite/Table.php @@ -0,0 +1,36 @@ +executeSql("PRAGMA table_info(`".$table_name."`);"); + + if (!$rows = $stmt->fetchAll(PDO::FETCH_ASSOC)) { + return $stmt; + } + + $table_data = $pri = $uni = []; + + $table_indexes = [ + 'pri' => [], + 'uni' => [] + ]; + + foreach ($rows as $row) { + $data = ['type' => $row['type']]; + $table_data[$row['name']] = $data; + + if ($row['pk']) + $table_indexes['pri'][] = $row['name']; + } + + return [$table_data, $table_indexes]; + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveRecord/Base.php b/lib/Rails/ActiveRecord/Base.php new file mode 100755 index 0000000..42a0ede --- /dev/null +++ b/lib/Rails/ActiveRecord/Base.php @@ -0,0 +1,1029 @@ +assignAttributes($attrs, $options); + $new_model->_create_do(); + return $new_model; + } + + /** + * Can receive parameters that will be directly passed to where(); + */ + static public function destroyAll() + { + $models = call_user_func_array(get_called_class() . '::where', func_get_args())->take(); + foreach ($models as $m) { + $m->destroy(); + } + return $models; + } + + # TODO + static public function deleteAll(array $conds) + { + + } + + static public function update($id, array $attrs) + { + $attrs_str = []; + foreach (array_keys($attrs) as $attr) + $attrs_str[] = '`'.$attr.'` = ?'; + $sql = "UPDATE `" . static::tableName() . "` SET " . implode(', ', $attrs_str) . " WHERE id = ?"; + array_unshift($attrs, $sql); + $attrs[] = $id; + static::connection()->executeSql($attrs); + } + + /** + * Searches if record exists by id. + * + * How to use: + * Model::exists(1); + * Model::exists(1, 2, 3); + * Model::exists([1, 2, 3]); + */ + static public function exists($params) + { + $query = self::none(); + + if (ctype_digit((string)$params)) { + $query->where(['id' => $params]); + } else { + if (is_array($params)) + $query->where('id IN (?)', $params); + else + $query->where('id IN (?)', func_get_args()); + } + + return $query->exists(); + } + + static public function countBySql() + { + $stmt = call_user_func_array([static::connection(), 'executeSql'], func_get_args()); + if (ActiveRecord::lastError()) + return false; + + $rows = $stmt->fetchAll(PDO::FETCH_NUM); + + if (isset($rows[0][0])) + return (int)$rows[0][0]; + else + return false; + } + + static public function maximum($attr) + { + return self::connection()->selectValue('SELECT MAX(' . $attr . ') FROM ' . static::tableName()); + } + + static public function I18n() + { + return Rails::application()->I18n(); + } + + static public function transaction($params = [], \Closure $block = null) + { + self::connection()->transaction($params, $block); + } + + static public function connectionName() + { + return null; + } + + /** + * If called class specifies a connection, such connection will be returned. + */ + static public function connection() + { + $name = static::connectionName(); + return ActiveRecord::connection($name); + } + + /** + * Public properties declared in a model class can be used to avoid defining + * setter and getter methods; the value will be set/get directly to/from it. + */ + static public function hasPublicProperty($propName) + { + $reflection = self::getReflection(); + return $reflection->hasProperty($propName) && $reflection->getProperty($propName)->isPublic(); + } + + /** + * It is sometimes required an empty collection. + */ + static public function emptyCollection() + { + return new Collection(); + } + + static protected function cn() + { + return get_called_class(); + } + + static protected function _registry() + { + if (!self::$registry) + self::$registry = new Registry(); + return self::$registry->model(get_called_class()); + } + + /** + * Creates and returns a non-empty model. + * This function is useful to centralize the creation of non-empty models, + * since isNewRecord must set to null by passing non_empty. + */ + static private function _create_model(array $data) + { + self::$preventInit = true; + $model = new static(); + $model->attributes = $data; + + # Check for primary_key and set init_attrs. + if (!static::table()->primaryKey()) { + $model->storedAttributes = $data; + } + $model->isNewRecord = false; + $model->_register(); + $model->init(); + + return $model; + } + + static private function _create_collection(array $data, $query = null) + { + $models = array(); + + foreach ($data as $d) + $models[] = self::_create_model($d); + + $coll = new Collection($models, $query); + return $coll; + } + + static private function getReflection($class = null) + { + if (!$class) { + $class = get_called_class(); + } + if (!isset(self::$cachedReflections[$class])) { + self::$cachedReflections[$class] = new \ReflectionClass($class); + } + return self::$cachedReflections[$class]; + } + + public function __construct(array $attrs = []) + { + $this->assignAttributes($attrs); + if (!self::$preventInit) { + $this->init(); + } else { + self::$preventInit = true; + } + } + + /** + * Checks for the called property in this order + * 1. Checks if property is an attribute. + * 2. Checks if called property is an already loaded association. + * 3. Checks if called property is an association. + * If none if these validations is true, the property is actually called + * in order to generate an error. + */ + public function __get($prop) + { + # See Config/default_config.php for more info. + // if (!Rails::config()->ar2) { + if (static::isAttribute($prop)) { + return $this->getAttribute($prop); + } elseif ($this->getAssociation($prop) !== null) { + return $this->loadedAssociations[$prop]; + } + + throw new Exception\RuntimeException( + sprintf("Tried to get unknown property %s::$%s", get_called_class(), $prop) + ); + // } + // # Force error/default behaviour + // return $this->$prop; + } + + /** + * 1. Checks for setter method and calls it if available. + * 2. If the property is an attribute, the value is set to it (i.e. default setter for attributes). + * 3. The value is set as object property. < This shouldn't happen! + */ + public function __set($prop, $val) + { + // if (!Rails::config()->ar2) { + if (static::isAttribute($prop)) { + $this->setAttribute($prop, $val); + // } elseif ($this->setterExists($prop)) { + // $this->_run_setter($prop, $val); + } else { + throw new Exception\RuntimeException( + sprintf("Trying to set undefined property %s", $prop) + ); + // # Default PHP behaviour. + // $this->$prop = $val; + } + // } else { + // if (static::isAttribute($prop)) { + // $this->setAttribute($prop, $val); + // } else { + // throw new Exception\RuntimeException( + // sprintf("Trying to set undefined property %s", $prop) + // ); + // } + // } + } + + public function __isset($prop) + { + return $this->issetAttribute($prop); + } + + /** + * Some model's features can be overloaded: + * 1. Check if an attribute changed with attributeNameChanged(); + * Calling this normally would be: attributeChanged('attribute_name'); + * 2. Get the previous value of an attribute with attributeNameWas(); + * Normally: attributeWas('attribute_name'); + * 3. Overload attribute setters. Models *should* define setter + * methods for each attribute, but they can be called overloadingly. The correct + * or expected name of the setter method is camel-cased, like setAttributeName(); + * 4. Overload scopes. + * 5. Overload associations. + * 6. Overload attribute getters. The expected method name is the camel-cased name of + * the attribute, like attributeName(), because the "get" prefix is omitted for getters. + * Models *should* define getter methods for each attribute. + */ + public function __call($method, $params) + { + # Overload attributeChange() + if (strlen($method) > 7 && substr($method, -7) == 'Changed') { + $attribute = static::properAttrName(substr($method, 0, -7)); + return $this->attributeChanged($attribute); + } + + # Overload attributeWas() + if (strlen($method) > 3 && substr($method, -3) == 'Was') { + $attribute = static::properAttrName(substr($method, 0, -3)); + return $this->attributeWas($attribute); + } + + # Overload attribute setter. + if (substr($method, 0, 3) == 'set') { + $attrName = substr($method, 3); + $underscored = Rails::services()->get('inflector')->underscore($method); + if (static::isAttribute($underscored)) { + $this->setAttribute($underscored, array_shift($params)); + return; + } + } + + # Overload scopes. + if ($rel = static::scope($method, $params)) { + return $rel; + } + + // # Overload associations. + // if ($this->getAssociation($method) !== null) { + // return $this->loadedAssociations[$method]; + // } + + // # Overload attributes. + // $underscored = Rails::services()->get('inflector')->underscore($method); + // if (static::isAttribute($underscored)) { + // return $this->getAttribute($underscored); + // } + + throw new Exception\BadMethodCallException( + sprintf("Call to undefined method %s::%s", get_called_class(), $method) + ); + } + + public function isNewRecord() + { + return $this->isNewRecord; + } + + public function updateAttributes(array $attrs) + { + $this->assignAttributes($attrs); + $this->runCallbacks('before_update'); + + /** + * iTODO: Must let know save() we're updating, so it will + * validate data with action "update" and not "save". + * Should separate save() and make this and update_attribute call + * something like update()? + */ + if ($this->save(['action' => 'update'])) { + $this->runCallbacks('after_update'); + return true; + } + return false; + } + + /** + * Directly passes the new value to the attributes array and then + * saves the record. + * + * @param string $attrName The name of the attribute. + * @param mixed $value The value for the attribute, + */ + public function updateAttribute($attrName, $value) + { + $this->attributes[$attrName] = $value; + return $this->save(['skip_validation' => true, 'skip_callbacks' => true, 'action' => 'update']); + } + + /** + * Save record + * + * Saves in the database the properties of the object that match + * the columns of the corresponding table in the database. + * + * Is the model is new, create() will be called instead. + * + * @array $values: If present, object will be updated according + * to this array, otherwise, according to its properties. + */ + public function save(array $opts = array()) + { + // if (empty($opts['skip_validation'])) { + // if (!$this->_validate_data(!empty($opts['action']) ? $opts['action'] : 'save')) + // return false; + // } + + if ($this->isNewRecord()) { + if (!$this->_create_do($opts)) + return false; + } else { + if (!$this->_validate_data('save')) + return false; + if (empty($opts['skip_callbacks'])) { + if (!$this->runCallbacks('before_save')) + return false; + } + if (!$this->_save_do($opts)) + return false; + if (empty($opts['skip_callbacks'])) { + $this->runCallbacks('after_save'); + } + } + return true; + } + + /** + * Delete + * + * Deletes row from database based on Primary keys. + */ + static public function delete() + { + // if ($this->_delete_from_db('delete')) { + // foreach (array_keys(get_object_vars($this)) as $p) + // unset($this->$p); + // return true; + // } + // return false; + } + + # Deletes current model from database but keeps model's properties. + public function destroy() + { + return $this->_delete_from_db('destroy'); + } + + public function errors() + { + if (!$this->errors) + $this->errors = new ActiveModel\Errors(); + return $this->errors; + } + + public function reload() + { + try { + $data = $this->_get_stored_data(); + } catch (Exception\ExceptionInterface $e) { + return false; + } + $cn = get_called_class(); + $refl = new \ReflectionClass($cn); + $reflProps = $refl->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED); + $defProps = $refl->getDefaultProperties(); + + foreach ($reflProps as $reflProp) { + if ($reflProp->getDeclaringClass() == $cn && !$reflProp->isStatic()) { + $this->{$reflProp->name} = $defProps[$reflProp->name]; + } + } + + $this->isNewRecord = false; + + $this->attributes = $data->attributes(); + // $this->init(); + + return true; + } + + public function asJson() + { + return $this->attributes(); + } + + public function toJson() + { + return json_encode($this->asJson()); + } + + # TODO: + public function toXml(array $params = []) + { + if (!isset($params['attributes'])) { + // $this->_merge_model_attributes(); + $attrs = $this->attributes(); + } else { + $attrs = $params['attributes']; + } + + !isset($params['root']) && $params['root'] = strtolower(str_replace('_', '-', self::cn())); + + if (!isset($params['builder'])) { + $xml = new \Rails\Xml\Xml($attrs, $params); + return $xml->output(); + } else { + $builder = $params['builder']; + unset($params['builder']); + $builder->build($attrs, $params); + } + } + + public function getAssociation($name) + { + if (isset($this->loadedAssociations[$name])) { + return $this->loadedAssociations[$name]; + } elseif ($assoc = $this->get_association_data($name)) { + $model = $this->_load_association($name, $assoc[0], $assoc[1]); + $this->loadedAssociations[$name] = $model; + return $this->loadedAssociations[$name]; + } + } + + /** + * *************************** + * Default protected methods { + * *************************** + * attrAccessible and attrProtected can be found in Base\Methods\AttributeMethods. + */ + + /** + * Code executed when initializing the object. + * Called by _create_model() and _create_do(). + */ + protected function init() + { + } + + protected function associations() + { + return []; + } + + /** + * Example: + * + * return [ + * // Validate attributes + * 'attribute_name' => [ + * 'property' => rules... + * ], + * + * // Passing a value or an index that isn't recognized as + * // attribute will be considered as custom validation method. + * 'methodName', + * 'method2Name' => [ 'on' => [ actions... ] ], + * ]; + * + * Custom validation methods must set the error manually. They aren't expected to + * return anything. + */ + protected function validations() + { + return []; + } + + protected function callbacks() + { + return []; + } + /* } */ + + /** + * Returns model's current data in the database or using storedAttributes. + */ + protected function _get_stored_data() + { + if (!($prim_key = static::table()->primaryKey()) && !$this->storedAttributes) { + throw new Exception\RuntimeException( + "Can't find data without primary key nor storedAttributes" + ); + } + + $query = static::none(); + + if ($prim_key) { + $query->where('`'.$prim_key.'` = ?', $this->$prim_key); + } else { + $model = new static(); + $cols_names = static::table()->columnNames(); + foreach ($this->storedAttributes as $name => $value) { + if (in_array($name, $cols_names)) { + $model->attributes[$name] = $value; + } + } + + if (!$model->attributes) + throw new Exception\RuntimeException( + "Model rebuilt from storedAttributes failed" + ); + else + return $model; + } + + $current = $query->first(); + + if (!$current) + throw new Exception\RuntimeException( + "Row not found in database (searched with storedAttributes)" + ); + else + return $current; + } + + protected function runCallbacks($callback_name) + { + $callbacks = array(); + + $tmp = $this->callbacks(); + if (isset($tmp[$callback_name])) { + $callbacks = $tmp[$callback_name]; + } + + $callbacks = array_unique(array_filter(array_merge($callbacks, $this->_get_parents_callbacks($callback_name)))); + + if ($callbacks) { + foreach ($callbacks as $method) { + if (false === $this->$method()) { + return false; + } + } + } + + return true; + } + + /** + * @param array|Closure $params - Additional parameters to customize the query for the association + */ + private function _load_association($prop, $type, $params) + { + return $this->{'_find_' . $type}($prop, $params); + } + + private function _get_parents_callbacks($callback_name) + { + $all_callbacks = array(); + if (($class = get_parent_class($this)) != 'Rails\ActiveRecord\Base') { + $class = self::cn(); + $obj = new $class(); + while (($class = get_parent_class($obj)) != 'Rails\ActiveRecord\Base') { + $obj = new $class(); + if ($callbacks = $obj->callbacks()) { + if (isset($callbacks[$callback_name])) + $all_callbacks = array_merge($callbacks, $callbacks[$callback_name]); + } + } + } + return $all_callbacks; + } + + # Returns association property names. + private function _associations_names() + { + $associations = array(); + foreach ($this->associations() as $assocs) { + foreach ($assocs as $k => $v) + $associations[] = is_int($k) ? $v : $k; + } + return $associations; + } + + /** + * @return bool + * @see validations() + */ + private function _validate_data($action) + { + if (!$this->runCallbacks('before_validation')) + return false; + + $validation_success = true; + $modelClass = get_called_class(); + $classProps = get_class_vars($modelClass); + + foreach ($this->validations() as $attrName => $validations) { + /** + * This should only happen when passing a custom validation method with + * no validation options. + */ + if (is_int($attrName)) { + $attrName = $validations; + $validations = []; + } + + if (static::isAttribute($attrName) || array_key_exists($attrName, $classProps)) { + foreach ($validations as $type => $params) { + if (!is_array($params)) { + $params = [$params]; + } + + if ($modelClass::isAttribute($attrName)) { + $value = $this->getAttribute($attrName); + } else { + $value = $this->$attrName; + } + + $validation = new Validator($type, $value, $params); + $validation->set_params($action, $this, $attrName); + + if (!$validation->validate()->success()) { + $validation->set_error_message(); + $validation_success = false; + } + } + } else { + /** + * The attrName passed isn't an attribute nor a property, so we assume it's a method. + * + * $attrName becomes the name of the method. + * $validations becomes validation options. + */ + if (!empty($validations['on']) && !in_array($action, $validations['on'])) { + continue; + } + // $this->getAttribute($attrName); + $this->$attrName(); + if ($this->errors()->any()) { + $validation_success = false; + } + } + } + return $validation_success; + } + + private function _create_do() + { + if (!$this->runCallbacks('before_validation_on_create')) { + return false; + } elseif (!$this->_validate_data('create')) { + return false; + } + + $this->runCallbacks('after_validation_on_create'); + + if (!$this->runCallbacks('before_save')) + return false; + + if (!$this->runCallbacks('before_create')) + return false; + + $this->_check_time_column('created_at'); + $this->_check_time_column('updated_at'); + $this->_check_time_column('created_on'); + $this->_check_time_column('updated_on'); + + $cols_values = $cols_names = array(); + + // $this->_merge_model_attributes(); + + foreach ($this->attributes() as $attr => $val) { + $proper = static::properAttrName($attr); + if (!static::table()->columnExists($proper)) { + continue; + } + $cols_names[] = '`'.$attr.'`'; + $cols_values[] = $val; + $init_attrs[$attr] = $val; + } + + if (!$cols_values) + return false; + + $binding_marks = implode(', ', array_fill(0, (count($cols_names)), '?')); + $cols_names = implode(', ', $cols_names); + + $sql = 'INSERT INTO `'.static::tableName().'` ('.$cols_names.') VALUES ('.$binding_marks.')'; + + array_unshift($cols_values, $sql); + + static::connection()->executeSql($cols_values); + + $id = static::connection()->lastInsertId(); + + $primary_key = static::table()->primaryKey(); + + if ($primary_key && count($primary_key) == 1) { + if (!$id) { + $this->errors()->addToBase('Couldn\'t retrieve new primary key.'); + return false; + } + + if ($pri_key = static::table()->primaryKey()) { + $this->setAttribute($pri_key, $id); + } + } else { + $this->storedAttributes = $init_attrs; + } + + $this->isNewRecord = false; + // $this->init(); + + $this->runCallbacks('after_create'); + $this->runCallbacks('after_save'); + + return true; + } + + private function _save_do() + { + $w = $wd = $q = $d = array(); + + $dt = static::table()->columnNames(); + + try { + $current = $this->_get_stored_data(); + } catch (Exception\ExceptionInterface $e) { + $this->errors()->addToBase($e->getMessage()); + return; + } + + $has_primary_keys = false; + + foreach (static::table()->indexes() as $idx) { + $w[] = '`'.$idx.'` = ?'; + $wd[] = $current->$idx; + } + + if ($w) { + $has_primary_keys = true; + } + + // $this->_merge_model_attributes(); + + foreach ($this->attributes() as $prop => $val) { + # Can't update properties that don't have a column in DB, or + # PRImary keys, or time columns. + if (!in_array($prop, $dt) || $prop == 'created_at' || $prop == 'updated_at' || + $prop == 'created_on' || $prop == 'updated_on' + ) { + continue; + } elseif (!$has_primary_keys) { + $w[] = '`'.$prop.'` = ?'; + $wd[] = $current->$prop; + // foreach ($current->attributes() as $ + } + // } elseif (!$has_primary_keys && $val === $current->$prop) { + // $w[] = '`'.$prop.'` = ?'; + // $wd[] = $current->$prop; + + // } else + if ($val != $current->$prop) { + $this->setChangedAttribute($prop, $current->$prop); + $q[] = '`'.$prop.'` = ?'; + $d[] = $val; + } + } + + # Update `updated_at|on` if exists. + if ($this->_check_time_column('updated_at')) { + $q[] = "`updated_at` = ?"; + $d[] = $this->updated_at; + } elseif ($this->_check_time_column('updated_on')) { + $q[] = "`updated_on` = ?"; + $d[] = $this->updated_on; + } + + if ($q) { + $q = "UPDATE `" . static::tableName() . "` SET " . implode(', ', $q); + + $w && $q .= ' WHERE '.implode(' AND ', $w); + + $q .= ' LIMIT 1'; + + $d = array_merge($d, $wd); + array_unshift($d, $q); + + static::connection()->executeSql($d); + + if (ActiveRecord::lastError()) { + $this->errors()->addToBase(ActiveRecord::lastError()); + return false; + } + } + + $this->_update_init_attrs(); + return true; + } + + private function _delete_from_db($type) + { + if (!$this->runCallbacks('before_'.$type)) { + return false; + } + + $w = $wd = []; + + if ($keys = self::table()->indexes()) { + foreach ($keys as $k) { + $w[] = '`' . static::tableName() . '`.`'.$k.'` = ?'; + $wd[] = $this->$k; + } + } elseif ($this->storedAttributes) { + foreach ($this->storedAttributes as $attr => $val) { + $w[] = '`'.$attr.'` = ?'; + $wd[] = $val; + } + } else { + throw new Exception\LogicException( + "Can't delete model without attributes" + ); + } + + $w = implode(' AND ', $w); + + $query = 'DELETE FROM `' . static::tableName() . '` WHERE '.$w; + array_unshift($wd, $query); + + static::connection()->executeSql($wd); + + $this->runCallbacks('after_'.$type); + + return true; + } + + private function get_association_data($prop) + { + if ($assocs = $this->associations()) { + foreach ($assocs as $type => $assoc) { + foreach ($assoc as $name => $params) { + if (is_int($name)) { + $name = $params; + $params = array(); + } + + if ($name == $prop) { + return array($type, $params); + } + } + } + } + return false; + } + + private function _register() + { + self::_registry()->register($this); + } + + /** + * Check time column + * + * Called by save() and create(), checks if $column + * exists and automatically sets a value to it. + */ + private function _check_time_column($column) + { + if (!static::table()->columnExists($column)) + return false; + + $type = static::table()->columnType($column); + + if ($type == 'datetime') + $time = date('Y-m-d H:i:s'); + elseif ($type == 'year') + $time = date('Y'); + elseif ($type == 'date') + $time = date('Y-m-d'); + elseif ($type == 'time') + $time = date('H:i:s'); + elseif ($type == 'timestamp') + $time = time(); + else + return false; + + $this->attributes[$column] = $time; + + return true; + } + + private function _update_init_attrs() + { + foreach (array_keys($this->storedAttributes) as $name) { + if (isset($this->attributes[$name])) + $this->storedAttributes[$name] = $this->attributes[$name]; + } + } + + private function _getter_for($prop) + { + $camelized = Rails::services()->get('inflector')->camelize($prop); + $method = 'get' . ucfirst($camelized); + + if (method_exists($this, $method)) { + return $this->$method(); + } elseif (method_exists($this, $camelized)) { + return $this->$camelized(); + } + return null; + } + + private function setterExists($attrName) + { + $inflector = Rails::services()->get('inflector'); + $reflection = self::getReflection(); + + $setter = 'set' . $inflector->camelize($attrName); + + if ($reflection->hasMethod($setter) && $reflection->getMethod($setter)->isPublic()) { + return $setter; + } else { + false; + } + } +} diff --git a/lib/Rails/ActiveRecord/Base/Methods/AssociationMethods.php b/lib/Rails/ActiveRecord/Base/Methods/AssociationMethods.php new file mode 100755 index 0000000..b3d9bbc --- /dev/null +++ b/lib/Rails/ActiveRecord/Base/Methods/AssociationMethods.php @@ -0,0 +1 @@ +createdAt() + * The corresponding column for this attribute would be "created_at", + * therefore, the attribute name will be converted. + * For some cases, to disable the camel to lower conversion, + * this property can be set to false. + */ + static protected $convertAttributeNames = true; + + /** + * Expected to hold only the model's attributes. + */ + protected $attributes = []; + + /** + * Holds data grabbed from the database for models + * without a primary key, to be able to update them. + * Hoever, models should always have a primary key. + */ + private $storedAttributes = array(); + + private $changedAttributes = array(); + + static public function convertAttributeNames($value = null) + { + if (null !== $value) { + static::$convertAttributeNames = (bool)$value; + } else { + return static::$convertAttributeNames; + } + } + + static public function isAttribute($name) + { + // if (!Rails::config()->ar2) { + // return static::table()->columnExists(static::properAttrName($name)); + // } else { + return static::table()->columnExists($name); + // } + } + + /** + * This method allows to "overloadingly" get attributes this way: + * $model->parentId; instead of $model->parent_id. + */ + static public function properAttrName($name) + { + if (static::convertAttributeNames()) { + $name = \Rails::services()->get('inflector')->underscore($name); + } + return $name; + } + + /** + * @throw Exception\InvalidArgumentException + */ + public function getAttribute($name) + { + if (array_key_exists($name, $this->attributes)) { + return $this->attributes[$name]; + // } elseif (!Rails::config()->ar2 && static::table()->columnExists(static::properAttrName($name))) { + // return null; + } elseif (static::table()->columnExists($name)) { + return null; + } + + throw new Exception\InvalidArgumentException( + sprintf("Trying to get non-attribute '%s' from model %s", $name, get_called_class()) + ); + } + + public function setAttribute($name, $value) + { + if (!self::isAttribute($name)) { + throw new Exception\InvalidArgumentException( + sprintf("Trying to set non-attribute '%s' for model %s", $name, get_called_class()) + ); + } + + if ((string)$this->getAttribute($name) != (string)$value) { + $this->setChangedAttribute($name, $value); + } + $this->attributes[$name] = $value; + return $this; + } + + public function issetAttribute($name) + { + if (!self::isAttribute($name)) { + throw new Exception\InvalidArgumentException( + sprintf("'%s' isn't an attribute for model %s", $name, get_called_class()) + ); + } + + return isset($this->attributes[$name]); + } + + /** + * Add/change attributes to model + * + * Filters protected attributes of the model. + * Also calls the "getAttribute()" method, if exists, of the model, + * in case extra operation is needed when changing certain attribute. + * It's intended to be an equivalent to "def attribute=(val)" in rails. + * E.g. "is_held" for post model. + * + * @see _run_setter() + */ + public function assignAttributes(array $attrs, array $options = []) + { + if (!$attrs) { + return; + } + + if (empty($options['without_protection'])) { + $this->filterProtectedAttributes($attrs); + } + + // if (!Rails::config()->ar2) { + // foreach ($attrs as $attr => $v) { + // if ($this->setterExists($attr)) { + // $this->_run_setter($attr, $v); + // } else { + // $this->$attr = $v; + // } + // } + // return; + // } + + // $inflector = Rails::services()->get('inflector'); + // $reflection = new \ReflectionClass(get_called_class()); + + foreach ($attrs as $attrName => $value) { + if (self::isAttribute($attrName)) { + $this->setAttribute($attrName, $value); + } else { + if ($setterName = $this->setterExists($attrName)) { + $this->$setterName($value); + // $setter = 'set' . $inflector->camelize($attrName); + } elseif (self::hasPublicProperty($attrName)) { + $this->$attrName = $value; + // if ($reflection->hasMethod($setter) && $reflection->getMethod($setter)->isPublic()) { + // $this->$setter($value); + } else { + throw new Exception\RuntimeException( + sprintf("Can't write unknown attribute '%s' for model %s", $attrName, get_called_class()) + ); + } + } + } + } + + public function attributes() + { + return $this->attributes; + } + + /** + * The changedAttributes array is filled upon updating a record. + * When updating, the stored data of the model is retrieved and checked + * against the data that will be saved. If an attribute changed, the old value + * is stored in this array. + * + * Calling a method that isn't defined, ending in Changed, for example nameChanged() or + * categoryIdChanged(), is the same as calling attributeChanged('name') or + * attributeChanged('category_id'). + * + * @return bool + * @see attributeWas() + */ + public function attributeChanged($attr) + { + return array_key_exists($attr, $this->changedAttributes); + } + + /** + * This method returns the previous value of an attribute before updating a record. If + * it was not changed, returns null. + */ + public function attributeWas($attr) + { + return $this->attributeChanged($attr) ? $this->changedAttributes[$attr] : null; + } + + public function changedAttributes() + { + return $this->changedAttributes; + } + + /** + * List of the attributes that can't be changed in the model through + * assignAttributes(). + * If both attrAccessible() and attrProtected() are present in the model, + * only attrAccessible() will be used. + * + * Return an empty array so no attributes are protected (except the default ones). + */ + protected function attrProtected() + { + return null; + } + + /** + * List of the only attributes that can be changed in the model through + * assignAttributes(). + * If both attrAccessible() and attrProtected() are present in the model, + * only attrAccessible() will be used. + * + * Return an empty array so no attributes are accessible. + */ + protected function attrAccessible() + { + return null; + } + + protected function setChangedAttribute($attr, $oldValue) + { + $this->changedAttributes[$attr] = $oldValue; + } + + private function filterProtectedAttributes(&$attributes) + { + # Default protected attributes + $default_columns = ['created_at', 'updated_at', 'created_on', 'updated_on']; + + if ($pk = static::table()->primaryKey()) { + $default_columns[] = $pk; + } + + $default_protected = array_fill_keys(array_merge($default_columns, $this->_associations_names()), true); + $attributes = array_diff_key($attributes, $default_protected); + + if (is_array($attrs = $this->attrAccessible())) { + $attributes = array_intersect_key($attributes, array_fill_keys($attrs, true)); + } elseif (is_array($attrs = $this->attrProtected())) { + $attributes = array_diff_key($attributes, array_fill_keys($attrs, true)); + } + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveRecord/Base/Methods/CounterMethods.php b/lib/Rails/ActiveRecord/Base/Methods/CounterMethods.php new file mode 100755 index 0000000..bdb35b0 --- /dev/null +++ b/lib/Rails/ActiveRecord/Base/Methods/CounterMethods.php @@ -0,0 +1,26 @@ + 1]); + } + + static public function decrementCounter($counter_name, $id) + { + return self::updateCounters([$id], [$counter_name => -1]); + } + + static public function updateCounters(array $ids, array $counters) + { + if (!is_array($ids)) + $ids = [$ids]; + $values = []; + foreach ($counters as $name => $value) + $values[] = "`" . $name . "` = `".$name."` " . ($value > 0 ? '+' : '-') . " 1"; + $sql = "UPDATE `".self::tableName()."` SET ".implode(', ', $values)." WHERE id IN (?)"; + return self::connection()->executeSql($sql, $ids); + } +} diff --git a/lib/Rails/ActiveRecord/Base/Methods/ModelSchemaMethods.php b/lib/Rails/ActiveRecord/Base/Methods/ModelSchemaMethods.php new file mode 100755 index 0000000..60e7082 --- /dev/null +++ b/lib/Rails/ActiveRecord/Base/Methods/ModelSchemaMethods.php @@ -0,0 +1,51 @@ +reloadSchema(); + self::$tables[$cn] = $table; + } + return self::$tables[$cn]; + } + + static public function tableName() + { + $cn = str_replace('\\', '_', get_called_class()); + $inf = \Rails::services()->get('inflector'); + + $tableName = $inf->underscore($inf->pluralize($cn)); + + return static::tableNamePrefix() . $tableName . static::tableNameSuffix(); + } + + static public function tableNamePrefix() + { + return ''; + } + + static public function tableNameSuffix() + { + return ''; + } + + /** + * @return ModelSchema + */ + static protected function initTable() + { + return new ModelSchema(static::tableName(), static::connection()); + } + +} \ No newline at end of file diff --git a/lib/Rails/ActiveRecord/Base/Methods/RelationMethods.php b/lib/Rails/ActiveRecord/Base/Methods/RelationMethods.php new file mode 100755 index 0000000..7861696 --- /dev/null +++ b/lib/Rails/ActiveRecord/Base/Methods/RelationMethods.php @@ -0,0 +1,240 @@ + $id])->first()) + return $model; + throw new Rails\ActiveRecord\Exception\RecordNotFoundException( + sprintf("Couldn't find %s with id = %d.", self::cn(), $id) + ); + } + + /** + * "Instantiator" methods { + */ + static public function from() + { + return self::createRelation('select', func_get_args()); + } + + static public function group() + { + return self::createRelation('select', func_get_args()); + } + + static public function having() + { + return self::createRelation('having', func_get_args()); + } + + static public function joins() + { + return self::createRelation('joins', func_get_args()); + } + + static public function limit() + { + return self::createRelation('limit', func_get_args()); + } + + # TODO + static public function unscoped() + { + return new Relation(get_called_class(), static::tableName()); + } + + /** + * This is the correct method to instantiate an empty relation. + */ + static public function none() + { + // $cn = self::cn(); + // return new Relation($cn, static::tableName()); + return self::createRelation(); + } + + static public function offset() + { + return self::createRelation('offset', func_get_args()); + } + + static public function order() + { + return self::createRelation('order', func_get_args()); + } + + static public function select() + { + return self::createRelation('select', func_get_args()); + } + + static public function distinct() + { + return self::createRelation('distinct', func_get_args()); + } + + static public function where() + { + return self::createRelation('where', func_get_args()); + } + /** + * } + */ + + /** + * Take all records. + */ + static public function all() + { + return self::none()->take(); + } + + /** + * For directly pagination without conditions. + */ + static public function paginate($page, $per_page) + { + $query = self::createRelation(); + return $query->paginate($page, $per_page); + } + + /** + * @return ActiveRecord\Collection + */ + static public function createModelsFromQuery(AbstractRelation $query) + { + if ($query->will_paginate()) { + $params = [ + 'page' => $query->get_page(), + 'perPage' => $query->get_per_page(), + 'offset' => $query->get_offset(), + 'totalRows' => $query->get_row_count() + ]; + } else + $params = []; + + return self::_create_collection($query->get_results()->fetchAll(PDO::FETCH_ASSOC), $params); + } + + static protected function createRelation($init_method = null, array $init_args = []) + { + $cn = self::cn(); + $query = new Relation($cn, static::tableName()); + if ($init_method) + call_user_func_array([$query, $init_method], $init_args); + return $query; + } + + /** + * When calling this method for pagination, how do we tell it + * the values for page and per_page? That's what extra_params is for... + * Although maybe it's not the most elegant solution. + * extra_params accepts 'page' and 'per_page', they will be sent to Query + * where they will be parsed. + */ + static public function findBySql($sql, array $params = array(), array $extra_params = array()) + { + $query = self::createRelation(); + + $query->complete_sql($sql, $params, $extra_params); + + return $query->take(); + } + + private function _find_has_one($prop, array $params) + { + empty($params['class_name']) && $params['class_name'] = Rails::services()->get('inflector')->camelize($prop); + + $builder = new Association($params, $this); + $builder->build_query(); + return $builder->get_query()->first() ?: false; + } + + /** + * @param array $params - Additional parameters to customize the query for the association + */ + private function _find_has_many($prop, $params) + { + empty($params['class_name']) && $params['class_name'] = rtrim(ucfirst($prop), 's'); + + $builder = new Association($params, $this); + $builder->build_query(); + + return $builder->get_query()->take() ?: false; + } + + private function _find_belongs_to($prop, array $params) + { + empty($params['class_name']) && $params['class_name'] = ucfirst($prop); + + $foreign_key = !empty($params['foreign_key']) ? $params['foreign_key'] : Rails::services()->get('inflector')->underscore($prop) . '_id'; + + if ($this->getAttribute($foreign_key)) { + return $params['class_name']::where(['id' => $this->getAttribute($foreign_key)])->first() ?: false; + } else { + return false; + } + } + + private function _find_has_and_belongs_to_many($prop, array $params) + { + $find_params = []; + + empty($params['class_name']) && $params['class_name'] = ucfirst(Rails::services()->get('inflector')->singularize($prop)); + + $find_params['class_name'] = $params['class_name']; + $find_params['from'] = $find_params['class_name']::tableName(); + + empty($find_params['join_table']) && $find_params['join_table'] = $find_params['class_name']::tableName() . '_' . $find_params['from']; + + empty($find_params['join_type']) && $find_params['join_type'] = 'join'; + empty($find_params['join_table_key']) && $find_params['join_table_key'] = 'id'; + empty($find_params['association_foreign_key']) && $find_params['association_foreign_key'] = substr($find_params['from'], 0, -1) . '_id'; + + $joins = strtoupper($find_params['join_type']) . ' `' . $find_params['join_table'] . '` ON `' . $find_params['from'] . '`.`' . $find_params['join_table_key'] . '` = `' . $find_params['join_table'] . '`.`' . $find_params['association_foreign_key'] . '`'; + + ## Having Post model with has_and_belongs_to_many => tags (Tag model): + // 'join_table' => posts_tags [not this => (or tags_posts)] + // 'join_table_key' => id + // 'foreign_key' => post_id + // 'association_foreign_key' => tag_id + + # Needed SQL params + // 'select' => posts.* + // 'joins' => JOIN posts_tags ON posts.id = posts_tags.post_id', + // 'conditions' => array('posts_tags.post_id = ?', $this->id) + + /** + + 'has_and_belongs_to_many' => [ + 'tags' => [ + 'join_type' => 'join', + 'join_table_key' => 'id', + 'foreign_key' => 'post_id', + 'association_foreign_key' => 'tag_id', + 'select' => 'posts.*' + ] + ] + + */ + $relation = new Association($find_params, $this); + $relation->build_query(); + $relation->get_query()->joins($joins); + return $relation->get_query()->take() ?: false; + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveRecord/Base/Methods/ScopingMethods.php b/lib/Rails/ActiveRecord/Base/Methods/ScopingMethods.php new file mode 100755 index 0000000..4fae004 --- /dev/null +++ b/lib/Rails/ActiveRecord/Base/Methods/ScopingMethods.php @@ -0,0 +1,63 @@ +scopes(); + + if (isset($scopes[$name])) { + $lambda = $scopes[$name]; + $relation = new Relation($cn, static::tableName()); + + $lambda = $lambda->bindTo($relation); + call_user_func_array($lambda, $params); + return $relation; + } else + return false; + } + + /** + * Defines scopes. + * + * Eg: + * + class Book extends ActiveRecord\Base + { + static protected function scopes() + { + return [ + 'fantasy' => function() { + $this->where(['genre' => 'fantasy']); + }, + 'available' => function() { + $this->where(['status' => 'available']); + }, + 'author' => function($author_name, $limit = 0) { + $this->where('author_name = ?', $author_name); + if ($limit) + $this->limit($limit); + } + ]; + } + } + * The anonymous function is binded to an ActiveRecord\Relation object. + * + * Not supporting default scope for now. + * Not supporting static methods as scopes, as they couldn't + * be called from a model instance. + */ + protected function scopes() + { + return []; + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveRecord/Collection.php b/lib/Rails/ActiveRecord/Collection.php new file mode 100755 index 0000000..7c51301 --- /dev/null +++ b/lib/Rails/ActiveRecord/Collection.php @@ -0,0 +1,73 @@ +members = $members; + $this->set_extra_params($data); + } + + public function currentPage() + { + return $this->page; + } + + public function perPage() + { + return $this->perPage; + } + + public function offset() + { + return $this->offset; + } + + public function totalPages() + { + return $this->totalPages; + } + + public function totalRows() + { + return $this->totalRows; + } + + public function previousPage() + { + return $this->page - 1 ?: false; + } + + public function nextPage() + { + return $this->page + 1 > $this->totalPages ? false : $this->page + 1; + } + + private function set_extra_params($params) + { + if ($params) { + $params = array_intersect_key($params, array_fill_keys(array('page', 'perPage', 'offset', 'totalRows'), null)); + foreach($params as $k => $v) { + $this->$k = (int)$v; + } + } + if ($this->totalRows && $this->perPage) { + $this->totalPages = (int)ceil($this->totalRows / $this->perPage); + } + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveRecord/Connection.php b/lib/Rails/ActiveRecord/Connection.php new file mode 100755 index 0000000..b89bd26 --- /dev/null +++ b/lib/Rails/ActiveRecord/Connection.php @@ -0,0 +1,265 @@ +default_connection_config(), $config); + + if (empty($config['dsn'])) { + if ($dsn = $this->build_dsn($config)) + $config['dsn'] = $dsn; + else + throw new Exception\ErrorException(sprintf("Not enough info to create DSN string for connection '%s'", $name)); + } + + $this->pdo = $this->create_connection($config); + } elseif ($config instanceof PDO) { + $this->pdo = $config; + } else { + throw new Exception\InvalidArgumentException('Connection must be either array or instance of PDO.'); + } + + $this->adapter_name = ActiveRecord::proper_adapter_name($this->pdo->getAttribute(PDO::ATTR_DRIVER_NAME)); + + $this->name = $name; + } + + protected function build_dsn($config) + { + if (!isset($config['adapter'])) + return; + + switch ($config['adapter']) { + case 'mysql': + $str = $config['adapter']; + + $params = []; + if (isset($config['host'])) + $params['host'] = $config['host']; + if (isset($config['database'])) + $params['dbname'] = $config['database']; + if (isset($config['port'])) + $params['port'] = $config['port']; + if (isset($config['charset'])) + $params['charset'] = $config['charset']; + + if (!isset($config['dsn_params'])) { + $config['dsn_params'] = []; + } + + $params = array_merge($params, $config['dsn_params']); + + if ($params) { + $str .= ':'; + foreach ($params as $key => $value) + $str .= $key . '=' . $value . ';'; + } + + return $str; + + case 'sqlite': + $str = $config['adapter']; + if (isset($config['database'])) { + $str .= ':' . Rails::root() . '/' . $config['database'] . ''; + } + return $str; + } + } + + protected function default_connection_config() + { + return [ + 'username' => null, + 'password' => null, + 'driver_options' => [], + 'pdo_attributes' => [] + ]; + } + + /** + * Executes the sql and returns the statement. + */ + public function executeSql() + { + $params = func_get_args(); + $sql = array_shift($params); + + if (!$sql) + throw new Exception\LogicException("Can't execute SQL without SQL"); + elseif (is_array($sql)) { + $params = $sql; + $sql = array_shift($params); + } + $this->_parse_query_multimark($sql, $params); + + if (!$stmt = $this->pdo->prepare($sql)) { + list($code, $drvrcode, $msg) = $this->pdo->errorInfo(); + $e = new Exception\QueryException( + sprintf("[PDOStatement error] [SQLSTATE %s] (%s) %s", $code, $drvrcode, $msg) + ); + throw $e; + } elseif (!$stmt->execute($params) && Rails::env() == 'development') { + list($code, $drvrcode, $msg) = $stmt->errorInfo(); + $e = new Exception\QueryException( + sprintf("[PDOStatement error] [SQLSTATE %s] (%s) %s", $code, $drvrcode, $msg) + ); + $e->setStatement($stmt, $params); + throw $e; + } + + $err = $stmt->errorInfo(); + + if ($err[2]) { + ActiveRecord::setLastError($err, $this->name); + $e = new Exception\QueryException( + sprintf("[SQLSTATE %s] (%s) %s", $err[0], (string)$err[1], $err[2]) + ); + $e->setStatement($stmt, $params); + + $msg = "Error on database query execution\n"; + $msg .= $e->getMessage(); + Rails::log()->message($msg); + } + return $stmt; + } + + /** + * Executes the sql and returns the results. + */ + public function query() + { + $stmt = call_user_func_array([$this, 'executeSql'], func_get_args()); + if (ActiveRecord::lastError()) + return false; + return $stmt->fetchAll(); + } + + public function select() + { + $stmt = call_user_func_array([$this, 'executeSql'], func_get_args()); + if (ActiveRecord::lastError()) + return false; + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + public function selectRow() + { + $stmt = call_user_func_array([$this, 'executeSql'], func_get_args()); + if (ActiveRecord::lastError()) + return false; + return $stmt->fetch(PDO::FETCH_ASSOC); + } + + # Use only when retrieving ONE column from multiple rows. + public function selectValues() + { + $stmt = call_user_func_array([$this, 'executeSql'], func_get_args()); + if (ActiveRecord::lastError()) + return false; + + $cols = array(); + if ($data = $stmt->fetchAll(PDO::FETCH_ASSOC)) { + foreach ($data as $d) + $cols[] = current($d); + } + return $cols; + } + + public function selectValue() + { + $stmt = call_user_func_array([$this, 'executeSql'], func_get_args()); + if (ActiveRecord::lastError()) + return false; + + if ($data = $stmt->fetch()) { + $data = array_shift($data); + } + return $data; + } + + public function lastInsertId() + { + return $this->pdo->lastInsertId(); + } + + public function transaction($params = [], \Closure $block = null) + { + if (!$block) { + $block = $params; + } + $this->resource()->beginTransaction(); + $block(); + $this->resource()->commit(); + } + + public function resource() + { + return $this->pdo; + } + + public function pdo() + { + return $this->pdo; + } + + public function adapterName() + { + return $this->adapter_name; + } + + public function name() + { + return $this->name; + } + + protected function _parse_query_multimark(&$query, array &$params) + { + # If the number of tokens isn't equal to parameters, ignore + # it and return. PDOStatement will trigger a Warning. + if (is_bool(strpos($query, '?')) || substr_count($query, '?') != count($params)) + return; + + $parts = explode('?', $query); + $parsed_params = array(); + + foreach ($params as $k => $v) { + if (is_array($v)) { + $k++; + $count = count($v); + $parts[$k] = ($count > 1 ? ', ' . implode(', ', array_fill(0, $count - 1, '?')) : '') . $parts[$k]; + $parsed_params = array_merge($parsed_params, $v); + } else { + $parsed_params[] = $v; + } + } + + $params = $parsed_params; + $query = implode('?', $parts); + } + + protected function create_connection($data) + { + $pdo = new PDO($data['dsn'], $data['username'], $data['password'], $data['driver_options']); + + if ($data['pdo_attributes']) { + foreach ($data['pdo_attributes'] as $attr => $val) + $pdo->setAttribute($attr, $val); + } + return $pdo; + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveRecord/Exception/BadMethodCallException.php b/lib/Rails/ActiveRecord/Exception/BadMethodCallException.php new file mode 100755 index 0000000..b380ad9 --- /dev/null +++ b/lib/Rails/ActiveRecord/Exception/BadMethodCallException.php @@ -0,0 +1,6 @@ +stmt = $stmt; + $this->values = $values; + return $this; + } + + public function postMessage() + { + $msg = ''; + if ($this->stmt) { + $msg .= "\nQuery: " . $this->stmt->queryString; + if ($this->values) + $msg .= "\nValues: " . var_export($this->values, true); + } + return $msg; + } +} diff --git a/lib/Rails/ActiveRecord/Exception/QueryException.php b/lib/Rails/ActiveRecord/Exception/QueryException.php new file mode 100755 index 0000000..9aeba71 --- /dev/null +++ b/lib/Rails/ActiveRecord/Exception/QueryException.php @@ -0,0 +1,7 @@ +name = $name; + $this->connection = $connection; + } + + public function name() + { + return $this->name; + } + + public function setName($name) + { + $this->name = $name; + $this->reloadSchema(); + } + + public function schemaFile() + { + $path = Rails::root() . '/db/table_schema/' . $this->connection_name(); + $file = $path . '/' . $this->name . '.php'; + return $file; + } + + public function reloadSchema() + { + $config = Rails::application()->config(); + + if ($config->active_record->use_cached_schema) { + $this->loadCachedSchema(); + } elseif (!$this->getSchema()) { + throw new Exception\RuntimeException( + sprintf("Couldn't find schema for %s: %s", $this->name, $this->error_stmt) + ); + } + } + + protected function loadCachedSchema() + { + $file = $this->schemaFile(); + if (!is_file($file)) { + throw new Exception\RuntimeException( + sprintf( + "Couldn't find schema file for table %s: %s", + $this->name, + $file + ) + ); + } + list($this->columns, $this->indexes) = require $file; + } + + protected function getSchema() + { + $adapter = self::adapterClass(); + $data = $adapter::fetchSchema($this->connection, $this->name); + + list($this->columns, $this->indexes) = $data; + + return true; + } + + protected function adapterClass() + { + $name = $this->connection->adapterName(); + return "Rails\ActiveRecord\Adapter\\" . $name . "\Table"; + } + + /** + * This method can be overwritten to return an array + * with the names of the columns. + */ + public function columnNames() + { + return array_keys($this->columns); + } + + public function columnType($column_name) + { + return $this->columns[$column_name]['type']; + } + + public function columnExists($column_name) + { + return !empty($this->columns[$column_name]); + } + + public function enumValues($column_name) + { + if (!isset($this->columns[$column_name])) { + throw new Exception\RuntimeException( + sprintf("Column name %s not found for table %s", $column_name, $this->name) + ); + } + if (strpos($this->columns[$column_name]['type'], 'enum') !== 0) { + throw new Exception\RuntimeException( + sprintf("Column %s.%s is not of type enum", $this->name, $column_name) + ); + } + return $this->columns[$column_name]['enum_values']; + } + + public function setPrimaryKey($key) + { + $this->primaryKey = $key; + } + + public function primaryKey() + { + if ($this->primaryKey) { + return $this->primaryKey; + } else { + $keys = $this->indexes('pri'); + if ($keys) { + return $keys[0]; + } + } + return false; + } + + public function indexes($type = null) + { + if (!$type) { + return $this->indexes ? current($this->indexes) : array(); + } else { + if (isset($this->indexes[$type])) + return $this->indexes[$type]; + } + return false; + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveRecord/Registry.php b/lib/Rails/ActiveRecord/Registry.php new file mode 100755 index 0000000..e044f42 --- /dev/null +++ b/lib/Rails/ActiveRecord/Registry.php @@ -0,0 +1,146 @@ + [ + 'Post' => array( + 'table_name' => 'posts', + 'table' => ActiveRecord_ModelTable instance, + 'instances' => array( + $model_1_id => $model_1, + $model_2_id => $model_2 + ) + ); + ] + * + * PriKey will be used for quick find. + * If we need to find the model by an attribute + * other than its PriKey... Maybe we could do + * the query, but then search in the registry + * for the model, now that we have the PK. If + * it's there, retrieve the model! else, add it. + */ + private $_reg = array(); + + /** + * Sets model to which methods will respond to. + */ + private $_current_model; + + /** + * @var string $model_name Model's class name + */ + public function model($model_name) + { + $connection = $this->connection(); + + if (!isset($this->_reg[$connection])) + $this->_reg[$connection] = []; + + if (!isset($this->_reg[$connection][$model_name])) { + $table_name = $model_name::tableName(); + $this->_reg[$connection][$model_name] = array( + 'table_name' => $table_name, + 'table' => null, + 'instances' => array() + ); + } + $this->_current_model = $model_name; + return $this; + } + + public function tableName() + { + $cn = $this->_current_model; + return $cn::tableName(); + } + + public function table() + { + if (empty($this->_reg[$this->connection()][$this->_current_model]['table'])) { + $cn = $this->get_table_class(); + $this->_reg[$this->connection()][$this->_current_model]['table'] = new $cn($this->tableName(), $this->_current_model); + } + return $this->_reg[$this->connection()][$this->_current_model]['table']; + } + + public function search($id) + { + $id = (string)$id; + if (isset($this->_reg[$this->connection()][$this->_current_model]) && isset($this->_reg[$this->connection()][$this->_current_model][$id])) + return $this->_reg[$this->connection()][$this->_current_model][$id]; + } + + public function register($model) + { + $cn = get_class($model); + + if (!$cn::table()->primaryKey()) { + return; + } elseif (!$model->getAttribute('id')) { + return; + } + + if (!isset($this->_reg[$this->connection()][$this->_current_model])) + $this->_reg[$this->connection()][$this->_current_model] = array(); + + $id = (string)$model->getAttribute('id'); + + $this->_reg[$this->connection()][$this->_current_model][$id] = $model; + return true; + } + + public function find_joining_table($table1, $table2) + { + if (isset(self::$_joining_tables[$table1])) + return self::$_joining_tables[$table1]; + + $tables = [ + $table1 . '_' . $table2, + $table2 . '_' . $table1 + ]; + + $sql = "SHOW TABLES LIKE ?"; + $stmt = ActiveRecord::connection()->prepare($sql); + foreach ($tables as $table) { + $stmt->execute([$table]); + if ($stmt->fetch(PDO::FETCH_NUM)) { + self::$_joining_tables[$table1] = $table; + self::$_joining_tables[$table2] = $table; + return $table; + } + } + return false; + } + + public function connection() + { + return ActiveRecord::activeConnectionName(); + } + + protected function get_table_class() + { + $model = $this->_current_model; + $adapter = ActiveRecord::proper_adapter_name($model::connection()->adapterName()); + $cn = 'Rails\ActiveRecord\Adapter\\' . $adapter . '\Table'; + return $cn; + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveRecord/Relation.php b/lib/Rails/ActiveRecord/Relation.php new file mode 100755 index 0000000..0789dcd --- /dev/null +++ b/lib/Rails/ActiveRecord/Relation.php @@ -0,0 +1,6 @@ +model_name = $model_name; + $this->from($table_name); + } + } + + public function __call($method, $params) + { + /** + * Check if method is a scope method in the model class. + */ + $cn = $this->model_name; + + if ($this->merge_with_scope($method, $params)) + return $this; + + throw new Exception\BadMethodCallException( + sprintf("Call to undefined method %s::%s()", __CLASS__, $method) + ); + } + + /** + * Excecutes the query. + * Returns all records. + * + * @return ActiveRecord\Collection + */ + public function take($limit = null) + { + if ($limit !== null) { + $this->limit($limit); + } + $this->executeSql(); + $cn = $this->model_name; + return $cn::createModelsFromQuery($this); + } + + /** + * Executes a pagination query, that will calculate found rows. + * Both parameters (page and per_page) must be set, else an Exception will be thrown. + * Parameters can be set on the fly or using page() and per_page(). + * + * @param int $page Current page number + * @param int $per_page Results per page + * @throw Exception + * @see page() + * @see per_page() + */ + public function paginate($page = null, $per_page = null) + { + if ($page !== null) + $this->page($page); + + if ($per_page !== null) + $this->perPage($per_page); + + if ($this->page === null) + throw new Exception\BadMethodCallException("Missing page parameter for pagination"); + elseif ($this->limit === null || $this->offset === null) + throw new Exception\BadMethodCallException("Missing per_page parameter for pagination"); + + $this->will_paginate = true; + + return $this->take(); + } + + /** + * For this to work, all :where parameters must have been passed like this: + * Model::where(["foo" => $foo, "bar" => $bar])->where(["baz" => $baz]); + */ + public function firstOrInitialize() + { + $model = $this->first(); + + if ($model) + return $model; + else { + $cn = $this->model_name; + $model = new $cn(); + + foreach ($this->where as $params) { + if (!is_array($params)) + throw new Exception\InvalidArgumentException("Invalid 'where' parameters passed for firstOrInitialize"); + + foreach ($params as $column => $value) + $model->$column = $value; + } + return $model; + } + } + + /** + * For this to work, all :where parameters must have been passed like this: + * Model::where(["foo" => $foo, "bar" => $bar])->where(["baz" => $baz]); + * + * A closure object may be passed to perform any additional actions (like adding + * additional properties to the model) before it is created. + */ + public function firstOrCreate(Closure $block = null) + { + $model = $this->first(); + + if ($model) { + return $model; + } else { + $cn = $this->model_name; + $model = new $cn(); + + foreach ($this->where as $params) { + if (!is_array($params)) { + throw new Exception\InvalidArgumentException("Invalid 'where' parameters passed for firstOrCreate"); + } + + foreach ($params as $column => $value) + $model->$column = $value; + } + + if ($block) + $block($model); + + $model->save(); + return $model; + } + } + + /** + * @return null|ActiveRecord_Base The records found or null + */ + public function first($limit = 1) + { + $this->limit = $limit; + + $collection = $this->take(); + + if ($limit == 1) { + if ($collection->any()) + return $collection[0]; + } else + return $collection; + } + + /** + * Accepts more than 1 argument. + */ + public function pluck($column) + { + $columns = func_get_args(); + $argv_count = func_num_args(); + + $collection = $this->take(); + $values = []; + + if ($collection->any()) { + if ($argv_count == 1) { + foreach ($collection as $model) + $values[] = $model->$column; + } else { + foreach ($collection as $model) { + $row_values = []; + foreach ($columns as $column) { + $row_values[] = $model->$column; + } + $values[] = $row_values; + } + } + } + + return $values; + } + + public function count() + { + $this->select('COUNT(*) as count_all'); + $this->executeSql(); + if (($rows = $this->builder->stmt()->fetchAll()) && isset($rows[0]['count_all'])) + return (int)$rows[0]['count_all']; + } + + public function exists() + { + return (bool)$this->count(); + } + + public function last() + { + $this->limit = 1; + + $collection = $this->take(); + if ($collection->any()) + return last($collection->members()); + } + + /** + * "Unsets" a clause. + * If the clause is invalid, it's just ignored. + * This method has a complementary method called "only", + * not yet implemented. + */ + public function except($clause) + { + // $prop = '_' . $clause; + + switch ($clause) { + case 'distinct': + case 'from': + case 'offset': + case 'limit': + case 'page': + case 'per_page': + $this->$clause = null; + break; + + case 'select': + case 'joins': + case 'where': + case 'where_params': + case 'order': + case 'group': + case 'having': + case 'having_params': + $this->$clause = []; + break; + } + return $this; + } + + /** + * Methods that modify the query { + */ + public function from($from) + { + $this->from = $from; + return $this; + } + + public function group() + { + $this->group = array_merge($this->group, func_get_args()); + return $this; + } + + /** + * How to use: + * $query->having("foo > ? AND bar < ?", $foo, $bar); + * $query->having("foo > :foo AND bar < :bar", ['foo' => $foo, 'bar' => $bar]); + * $query->having("foo > 15"); + */ + public function having() + { + $args = func_get_args(); + $count = count($args); + + if ($count == 1) { + $this->having[] = $args[0]; + } else { + $this->having[] = array_shift($args); + + if ($count == 2 && is_array($args[0])) + $args = $args[0]; + + if ($args) + $this->having_params = array_merge($this->having_params, $args); + } + return $this; + } + + public function joins($params) + { + if (!is_array($params)) + $params = [$params]; + $this->joins = array_merge($this->joins, $params); + + return $this; + } + + public function limit($limit) + { + $this->limit = $limit; + return $this; + } + + public function offset($offset) + { + $this->offset = $offset; + return $this; + } + + public function order() + { + $this->order = array_merge($this->order, func_get_args()); + return $this; + } + + public function select($params) + { + if (!is_array($params)) + $params = [$params]; + $this->select = array_merge($this->select, $params); + + return $this; + } + + public function distinct($value = true) + { + $this->distinct = $value; + return $this; + } + + /** + * $query->where("foo = ? AND bar = ?", $foo, $bar); + * $query->where("foo = ? AND bar = ?", $foo, [$bar, $baz]); + * $query->where("foo = :foo AND bar = :bar", ['foo' => $foo, 'bar' => $bar]); + * $query->where("foo = true"); + * + * Following 2 methods are basically the same, first is recommended over the second: + * $query->where(["foo" => $foo, "bar" => $bar]); + * $query->where("foo", true, "bar", $bar); + * + * Can't do: + * $query->where("foo = ? AND bar = ?", [$foo, $bar]); + * $query->where(["foo = ? AND bar = ?", $foo, $bar]); + * $query->where(["foo", $foo]); + */ + public function where() + { + $args = func_get_args(); + $count = count($args); + + if ($count == 1) { + if (is_array($args[0])) { + # This is expected to be a column => value associated array. + # In this case, the array is stored as is. + $this->where[] = $args[0]; + } else { + # This is expected to be a string. + $this->where[] = $args[0]; + } + } elseif ($count) { + # Case: $query->where("foo", true, "bar_baz", $bar); + if ($count >= 2 && is_int($count / 2) && (!is_array($args[1]) || Toolbox\ArrayTools::isIndexed($args[1])) && is_bool(strpos($args[0], ' '))) { + $where = []; + foreach ($args as $key => $value) { + $key++; + if ($key && !($key % 2)) { + $where[$next_key] = $value; + } else { + $next_key = $value; + } + } + $this->where[] = $where; + } else { + $this->where[] = array_shift($args); + + # Case: $query->where('foo => :foo', ['foo' => $foo]); + if ($count == 2 && is_array($args[0]) && !Toolbox\ArrayTools::isIndexed($args[0])) + $args = $args[0]; + + if ($args) + $this->where_params = array_merge($this->where_params, $args); + } + } + return $this; + } + /** + * } + */ + + public function page($page) + { + $this->page = $page; + return $this; + } + + /** + * Note that for this to work correctly, $page must be set. + */ + public function perPage($per_page) + { + $this->limit = $per_page; + $this->offset = ($this->page - 1) * $per_page; + return $this; + } + + /** + * The following public methods are supposed to be used only by Rails. + */ + public function will_paginate() + { + return $this->will_paginate; + } + + /** + * TODO: this method should be changed. + * Expected to be used only by the system, when calling find_by_sql(). + */ + public function complete_sql($sql = null, array $params = [], array $extra_params = []) + { + if (func_num_args()) { + if (isset($extra_params['page'])) + $this->page = $extra_params['page']; + if (isset($extra_params['perPage'])) + $this->limit = $extra_params['perPage']; + if (isset($extra_params['offset'])) + $this->offset = $extra_params['offset']; + if ($this->page && $this->limit) + $this->will_paginate = true; + + $this->complete_sql = [$sql, $params]; + return $this; + } else + return $this->complete_sql; + } + + public function get_page() + { + return $this->page; + } + + public function get_per_page() + { + return $this->limit; + } + + public function get_offset() + { + return $this->offset; + } + + public function get_row_count() + { + return $this->builder->row_count(); + } + + public function get_results() + { + return $this->builder->stmt(); + } + + public function merge(self $other) + { + $scalars = [ + 'distinct', + 'from', + 'offset', + 'limit', + 'page', + 'per_page', + ]; + + $arrays = [ + 'select', + 'joins', + 'where', + 'where_params', + 'order', + 'group', + 'having', + 'having_params', + ]; + + foreach ($scalars as $scalar) { + if ($other->$scalar !== null) { + $this->$scalar = $other->$scalar; + } + } + + foreach ($arrays as $array) { + $this->$array = array_merge($this->$array, $other->$array); + } + return $this; + } + + # This method is expected to be used only by Rails. + public function default_scoped($value) + { + $this->default_scoped = (bool)$value; + return $this; + } + + private function _set_model_connection() + { + if (defined($this->model_name . '::connection')) { + $this->previous_connection_name = ActiveRecord::activeConnectionName(); + $model_name = $this->model_name; + ActiveRecord::setConnection($model_name::connection); + return true; + } + } + + private function _restore_previous_connection() + { + if ($this->previous_connection_name) { + ActiveRecord::setConnection($this->previous_connection_name); + $this->previous_connection_name = null; + } + } + + protected function executeSql() + { + $this->_set_model_connection(); + $builder_class = $this->_get_builder_class(); + + $model_name = $this->model_name; + $model_connection = $model_name::connection(); + $this->builder = new $builder_class($model_connection); + + $this->builder->build_sql($this); + $this->builder->executeSql(); + + $this->_restore_previous_connection(); + } + + private function merge_with_scope($name, array $params) + { + $cn = $this->model_name; + + if ($relation = $cn::scope($name, $params)) { + $this->merge($relation); + return true; + } + + return false; + } + + private function _get_builder_class() + { + $cn = $this->model_name; + $class = '\Rails\ActiveRecord\Adapter\\'; + $class .= ActiveRecord::proper_adapter_name($cn::connection()->adapterName()); + $class .= '\QueryBuilder'; + return $class; + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveRecord/Relation/Association.php b/lib/Rails/ActiveRecord/Relation/Association.php new file mode 100755 index 0000000..535ab33 --- /dev/null +++ b/lib/Rails/ActiveRecord/Relation/Association.php @@ -0,0 +1,48 @@ +params = $params; + $this->parent_model = $parent_model; + } + + public function get_query() + { + return $this->query; + } + + public function build_query() + { + $params = $this->params; + + if (empty($params['foreign_key'])) { + $cn = get_class($this->parent_model); + $params['foreign_key'] = substr($cn::tableName(), 0, -1).'_id'; + } + + $query = new Relation($params['class_name'], $params['class_name']::tableName()); + + $query->where('`' . $params['foreign_key'] . "` = ?", $this->parent_model->id); + + # params[0], if present, it's an anonymous function to customize the relation. + # The function is binded to the relation object. + if (isset($this->params[0])) { + $lambda = array_shift($this->params); + $lambda->bindTo($this); + $lambda(); + } + + $this->query = $query; + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveRecord/Validation/Exception/ExceptionInterface.php b/lib/Rails/ActiveRecord/Validation/Exception/ExceptionInterface.php new file mode 100755 index 0000000..c8e5e3a --- /dev/null +++ b/lib/Rails/ActiveRecord/Validation/Exception/ExceptionInterface.php @@ -0,0 +1,6 @@ + array( + * 'length' => '5..', + * 'format' => function($name) { ... return true; }, + * ) + */ +class Validator extends RailsValidation +{ + private + $_model, + + $_action, + + $_property, + + /** + * Helps for validations that could have + * different messages (e.g. length (minimum, maximum, is)) + */ + $_error_message_type = 'default', + + $_error_message, + + $_continue_validation; + + public function set_params($action, Rails\ActiveRecord\Base $model, $property) + { + $this->_model = $model; + $this->_action = $action; + $this->_property = $property; + } + + public function validate() + { + // if (current($this->_params) instanceof \Closure) + // $this->_run_closure(); + // else + $this->_check_conditions(); + + if ($this->_continue_validation) + parent::validate(); + + return $this; + } + + public function set_error_message() + { + if (isset($this->_params['base_message'])) + $this->_model->errors()->addToBase($this->_params['base_message']); + else { + $this->_set_error_message(); + $this->_model->errors()->add($this->_property, $this->_error_message); + } + } + + protected function _validate_number() + { + if (isset($this->_params['allow_null']) && $this->_model->getAttribute($this->_property) === null) + return true; + else + return parent::_validate_number(); + } + + protected function _validate_length() + { + if (isset($this->_params['allow_null']) && $this->_model->getAttribute($this->_property) === null) + return true; + elseif (isset($this->_params['allow_blank']) && $this->_model->getAttribute($this->_property) === '') + return true; + else + return parent::_validate_length(); + } + + protected function _validate_uniqueness() + { + $cn = get_class($this->_model); + if ($this->_model->isNewRecord()) { + $query = $cn::where('`'.$this->_property.'` = ?', $this->_model->getAttribute($this->_property)); + } else { + $query = $cn::where('`'.$this->_property.'` = ? AND id != ?', $this->_model->getAttribute($this->_property), $this->_model->getAttribute('id')); + } + return !((bool)$query->first()); + } + + protected function _validate_presence() + { + $property = trim($this->_model->{$this->_property}); + return !empty($property); + } + + protected function _validate_confirmation() + { + $property = Rails::services()->get('inflector')->camelize($this->_property, false) . 'Confirmation'; + + if ($this->_model->$property === null) + return true; + + return (string)$this->_model->getAttribute($this->_property) == (string)$this->_model->$property; + } + + protected function _validate_acceptance() + { + return !empty($this->_model->{$this->_property}); + } + + private function _run_closure() + { + $closure = current($this->_params); + if ($closure($this->_model->{$this->_property}) === true) { + $this->_success = true; + } + } + + private function _check_conditions() + { + if (!isset($this->_params['on'])) + $this->_params['on'] = 'save'; + $this->_run_on(); + + if (isset($this->_params['if'])) + $this->_run_if(); + } + + private function _run_on() + { + if ($this->_params['on'] == 'save' || $this->_params['on'] == $this->_action) + $this->_continue_validation = true; + else + $this->_success = true; + } + + private function _run_if() + { + if (is_array($this->_params['if'])) { + foreach ($this->_params['if'] as $cond => $params) { + if ($params instanceof \Closure) { + if ($params() !== true) { + $this->_success = true; + $this->_continue_validation = false; + return; + } + } else { + switch ($cond) { + case 'property_exists': + if ($this->_model->$params === null) { + $this->_success = true; + $this->_continue_validation = false; + return; + } + break; + } + } + } + } else { + throw new Exception\RuntimeException( + sprintf("Validation condition must be an array, %s passed", gettype($this->_params['if'])) + ); + } + + $this->_continue_validation = true; + } + + private function _set_error_message() + { + $message = ''; + $this->_define_error_message_type(); + + if ($this->_error_message_type != 'default') { + if (isset($this->_params[$this->_error_message_type])) + $message = $this->_params[$this->_error_message_type]; + } + if (!$message) + $message = $this->_error_message(); + $this->_error_message = $message; + } + + private function _define_error_message_type() + { + switch ($this->_type) { + case 'length': + if ($this->_result == -1) + $msg_type = 'too_short'; + elseif ($this->_result == 1) + $msg_type = 'too_long'; + else + $msg_type = 'wrong_length'; + break; + default: + $msg_type = 'default'; + break; + } + $this->_error_message_type = $msg_type; + } + + private function _error_message() + { + switch ($this->_type) { + case 'number': + case 'length': + if ($this->_result == 2 && (!empty($this->_params['even']) || !empty($this->_params['odd']))) { + $type = !empty($this->_params['even']) ? "even" : "odd"; + $params = ["odd"]; + } else { + if (!empty($this->_params['in'])) { + if ($this->_result == -1) { + $params = ['greater_than_or_equal_to', ['count' => $this->_params['in'][0]]]; + } else { + $params = ['less_than_or_equal_to', ['count' => $this->_params['in'][1]]]; + } + } elseif (!empty($this->_params['is'])) { + $params = ['equal_to', ['count' => $this->_params['is']]]; + } elseif (!empty($this->_params['minimum'])) { + $params = ['greater_than_or_equal_to', ['count' => $this->_params['minimum']]]; + } elseif (!empty($this->_params['maximum'])) { + $params = ['less_than_or_equal_to', ['count' => $this->_params['maximum']]]; + } + } + break; + + case 'blank': + $params = ['blank']; + break; + + case 'uniqueness': + $params = ['uniqueness']; + break; + + case 'confirmation': + $params = ["confirmation"]; + break; + + default: + $params = ['invalid']; + break; + } + $params[0] = 'errors.messages.' . $params[0]; + return call_user_func_array([\Rails::application()->I18n(), 't'], $params); + } +} diff --git a/lib/Rails/ActiveSupport/Inflector/DefaultEnglishInflections.php b/lib/Rails/ActiveSupport/Inflector/DefaultEnglishInflections.php new file mode 100755 index 0000000..2e3387f --- /dev/null +++ b/lib/Rails/ActiveSupport/Inflector/DefaultEnglishInflections.php @@ -0,0 +1,82 @@ + '\1zes', + '/^(oxen)$/i' => '\1', + '/^(ox)$/i' => '\1en', + '/^(m|l)ice$/i' => '\1ice', + '/^(m|l)ouse$/i' => '\1ice', + '/(matr|vert|ind)(?:ix|ex)$/i' => '\1ices', + '/(x|ch|ss|sh)$/i' => '\1es', + '/([^aeiouy]|qu)y$/i' => '\1ies', + '/(hive)$/i' => '\1s', + '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves', + '/sis$/i' => 'ses', + '/([ti])a$/i' => '\1a', + '/([ti])um$/i' => '\1a', + '/(buffal|tomat)o$/i' => '\1oes', + '/(alias|status)$/i' => '\1es', + '/(bu)s$/i' => '\1ses', + '/(octop|vir)i$/i' => '\1i', + '/(octop|vir)us$/i' => '\1i', + '/^(ax|test)is$/i' => '\1es', + '/s$/i' => 's', + '/$/' => 's', + ]; + + protected $singulars = [ + '/(database)s$/i' => '\1', + '/(quiz)zes$/i' => '\1', + '/(matr)ices$/i' => '\1ix', + '/(vert|ind)ices$/i' => '\1ex', + '/^(ox)en/i' => '\1', + '/(alias|status)(es)?$/i' => '\1', + '/(octop|vir)(us|i)$/i' => '\1us', + '/^(a)x[ie]s$/i' => '\1xis', + '/(cris|test)(is|es)$/i' => '\1is', + '/(shoe)s$/i' => '\1', + '/(o)es$/i' => '\1', + '/(bus)(es)?$/i' => '\1', + '/^(m|l)ice$/i' => '\1ouse', + '/(x|ch|ss|sh)es$/i' => '\1', + '/(m)ovies$/i' => '\1ovie', + '/(s)eries$/i' => '\1eries', + '/([^aeiouy]|qu)ies$/i' => '\1y', + '/([lr])ves$/i' => '\1f', + '/(tive)s$/i' => '\1', + '/(hive)s$/i' => '\1', + '/([^f])ves$/i' => '\1fe', + '/(^analy)(sis|ses)$/i' => '\1sis', + '/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i' => '\1sis', + '/([ti])a$/i' => '\1um', + '/(n)ews$/i' => '\1ews', + '/(ss)$/i' => '\1', + '/s$/i' => '', + ]; + + protected $irregulars = [ + 'person' => 'people', + 'man' => 'men', + 'child' => 'children', + 'sex' => 'sexes', + 'move' => 'moves', + 'cow' => 'kine', + 'zombie' => 'zombies', + ]; + + protected $uncountables = [ + 'equipment', + 'information', + 'rice', + 'money', + 'species', + 'series', + 'fish', + 'sheep', + 'jeans', + 'police', + ]; +} \ No newline at end of file diff --git a/lib/Rails/ActiveSupport/Inflector/Inflections.php b/lib/Rails/ActiveSupport/Inflector/Inflections.php new file mode 100755 index 0000000..d77540c --- /dev/null +++ b/lib/Rails/ActiveSupport/Inflector/Inflections.php @@ -0,0 +1,132 @@ +acronyms[strtolower($word)] = $word; + $this->acronymRegex = '/' . implode('|', $this->acronymRegex) . '/'; + } + + public function plural($rule, $replacement) + { + unset($this->uncountables[$rule]); + unset($this->uncountables[$replacement]); + $this->plurals[$rule] = $replacement; + } + + public function singular($rule, $replacement) + { + unset($this->uncountables[$rule]); + unset($this->uncountables[$replacement]); + $this->singulars[$rule] = $replacement; + } + + public function irregular($singular, $plural) + { + unset($this->uncountables[$singular]); + unset($this->uncountables[$plural]); + + $s0 = $singular[0]; + $srest = substr($singular, 1, -1); + + $p0 = $plural[0]; + $prest = substr($plural, 1, -1); + + if (strtoupper($s0) == strtoupper($p0)) { + $this->plural("/($s0)$srest$/i", '\1' . $prest); + $this->plural("/($p0)$prest$/i", '\1' . $prest); + + $this->singular("/($s0)$srest$/i", '\1' . $srest); + $this->singular("/($p0)$prest$/i", '\1' . $srest); + } else { + $this->plural("/".strtoupper($s0)."(?i)$srest$/", strtoupper($p0) . $prest); + $this->plural("/".strtolower($s0)."(?i)$srest$/", strtolower($p0) . $prest); + $this->plural("/".strtoupper($p0)."(?i)$prest$/", strtoupper($p0) . $prest); + $this->plural("/".strtolower($p0)."(?i)$prest$/", strtolower($p0) . $prest); + + $this->singular("/".strtoupper($s0)."(?i)$srest$/", strtoupper($s0) . $srest); + $this->singular("/".strtolower($s0)."(?i)$srest$/", strtolower($s0) . $srest); + $this->singular("/".strtoupper($p0)."(?i)$prest$/", strtoupper($s0) . $srest); + $this->singular("/".strtolower($p0)."(?i)$prest$/", strtolower($s0) . $srest); + } + } + + public function uncountable() + { + $this->uncountables = array_merge($this->uncountables, func_get_args()); + } + + public function human($rule, $replacement) + { + $this->humans[$rule] = $replacement; + } + + public function clear($scope = 'all') + { + switch ($scope) { + case 'all': + $this->plurals = $this->singulars = $this->uncountables = $this->humans = []; + break; + + default: + if (!isset($this->$scope)) { + throw new Exception\InvalidArgumentException( + sprintf("Unknown scope to clear: %s", $scope) + ); + } + $this->$scope = []; + break; + } + } + + public function acronyms() + { + return $this->acronyms; + } + + public function plurals() + { + return $this->plurals; + } + + public function singulars() + { + return $this->singulars; + } + + public function irregulars() + { + return $this->irregulars; + } + + public function uncountables() + { + return $this->uncountables; + } + + public function humans() + { + return $this->humans; + } + + public function acronymRegex() + { + return $this->acronymRegex; + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveSupport/Inflector/Inflector.php b/lib/Rails/ActiveSupport/Inflector/Inflector.php new file mode 100755 index 0000000..18737be --- /dev/null +++ b/lib/Rails/ActiveSupport/Inflector/Inflector.php @@ -0,0 +1,201 @@ +inflections['en'] = new DefaultEnglishInflections(); + } + + public function inflections($locale = 'en', Closure $block = null) + { + if ($locale instanceof Closure) { + $block = $locale; + $locale = 'en'; + } + + $inflections = $this->getInflections($locale); + + if ($block) { + $block($inflections); + } else { + return $inflections; + } + } + + public function pluralize($word, $locale = 'en') + { + $irregulars = $this->inflections()->irregulars(); + if (isset($irregulars[$word])) { + return $this->inflections()->irregulars()[$word]; + } + return $this->applyInflections($word, $this->inflections($locale)->plurals()); + } + + public function singularize($word, $locale = 'en') + { + if (is_string($key = array_search($word, $this->inflections()->irregulars()))) { + return $key; + } + return $this->applyInflections($word, $this->inflections($locale)->singulars()); + } + + public function camelize($term, $uppercaseFirstLetter = true) + { + $string = (string)$term; + $acronyms = $this->inflections()->acronyms(); + + if ($uppercaseFirstLetter) { + $string = preg_replace_callback('/^[a-z\d]*/', function($m) use ($acronyms) { + if (isset($acronyms[$m[0]])) { + return $acronyms[$m[0]]; + } else { + return ucfirst($m[0]); + } + }, $string); + } else { + $acronymRegex = $this->inflections()->acronymRegex(); + $string = preg_replace_callback('/^(?:'.$acronymRegex.'(?=\b|[A-Z_])|\w)/', function($m) use($term) { + return strtolower($m[0]); + }, $string); + } + + return preg_replace_callback('/(?:_|(\/))([a-z\d]*)/i', function($m) use ($acronyms) { + if (isset($acronyms[$m[2]])) { + return $m[1] . $acronyms[$m[2]]; + } else { + return ucfirst($m[2]); + } + }, $string); + } + + public function underscore($camelCasedWord) + { + $word = (string)$camelCasedWord; + $word = preg_replace_callback('/(?:([A-Za-z\d])|^)(?=\b|[^a-z])/', function($m) use ($camelCasedWord) { + $ret = ''; + if (isset($m[1])) { + $ret = $m[1]; + } + if (isset($m[2])) { + $ret .= $m[2]; + } + + return $ret; + }, $word); + + $word = preg_replace([ + '/([A-Z\d]+)([A-Z][a-z])/', + '/([a-z\d])([A-Z])/' + ], [ + '\1_\2', + '\1_\2' + ], $word); + + $word = strtr($word, '-\\', '_/'); + $word = strtolower($word); + return $word; + } + + public function humanize($lowerCaseAndUnderscoredWord) + { + $result = (string)$lowerCaseAndUnderscoredWord; + foreach ($this->inflections()->humans() as $rule => $replacement) { + $ret = preg_replace($rule, $replacement, $result, -1, $count); + if ($count) { + $result = $ret; + break; + } + } + + if (strpos($result, '_id') === strlen($result) - 3) { + $result = substr($result, 0, -3); + } + $result = strtr($result, '_', ' '); + + $acronyms = $this->inflections()->acronyms(); + + $result = preg_replace_callback('/([a-z\d]*)/i', function($m) use ($acronyms) { + if (isset($acronyms[$m[1]])) { + return $acronyms[$m[1]]; + } else { + return strtolower($m[1]); + } + }, $result); + + $result = preg_replace_callback('/^\w/', function($m) { + return strtoupper($m[0]); + }, $result); + return $result; + } + + public function titleize($word) + { + return ucwords($this->humanize($this->underscore($word))); + } + + public function tableize($className) + { + return $this->pluralize($this->underscore($className)); + } + + public function classify($tableName) + { + return $this->camelize($this->singuralize(preg_replace('/.*\./', '', $tableName))); + } + + public function ordinal($number) + { + $absNumber = (int)$number; + if (in_array($absNumber % 100, range(11, 13))) { + return $absNumber . 'th'; + } else { + switch ($absNumber % 100) { + case 1: + return $absNumber . 'st'; + case 2: + return $absNumber . 'nd'; + case 3: + return $absNumber . 'rd'; + default: + return $absNumber . 'th'; + } + } + } + + protected function getInflections($locale) + { + if (!isset($this->inflections[$locale])) { + $this->inflections[$locale] = new Inflections(); + } + return $this->inflections[$locale]; + } + + protected function applyInflections($word, $rules) + { + if ( + !$word || + ( + preg_match('/\b\w+\Z/', strtolower($word), $m) && + in_array($m[0], $this->inflections()->uncountables()) + ) + ) { + return $word; + } else { + foreach ($rules as $rule => $replacement) { + $ret = preg_replace($rule, $replacement, $word, -1, $count); + if ($count) { + $word = $ret; + break; + } + } + return $word; + } + } +} \ No newline at end of file diff --git a/lib/Rails/ActiveSupport/Inflector/Word.php b/lib/Rails/ActiveSupport/Inflector/Word.php new file mode 100755 index 0000000..1aec2d3 --- /dev/null +++ b/lib/Rails/ActiveSupport/Inflector/Word.php @@ -0,0 +1,82 @@ +word = $word; + $this->inflector = $inflector; + $this->locale = $locale; + } + + public function __toString() + { + return $this->word; + } + + public function setLocale($locale) + { + $this->locale = $locale; + } + + public function pluralize() + { + $this->word = $this->inflector->pluralize($this->word, $this->locale); + return $this; + } + + public function singularize() + { + $this->word = $this->inflector->singularize($this->word, $this->locale); + return $this; + } + + public function camelize($uppercaseFirstLetter = true) + { + $this->word = $this->inflector->camelize($this->word, $uppercaseFirstLetter); + return $this; + } + + public function underscore() + { + $this->word = $this->inflector->underscore($this->word); + return $this; + } + + public function humanize() + { + $this->word = $this->inflector->humanize($this->word); + return $this; + } + + public function titleize() + { + $this->word = $this->inflector->titleize($this->word); + return $this; + } + + public function tableize() + { + $this->word = $this->inflector->tableize($this->word); + return $this; + } + + public function classify() + { + $this->word = $this->inflector->tableize($this->word); + return $this; + } + + public function ordinal() + { + $this->word = $this->inflector->ordinal($this->word); + return $this; + } +} \ No newline at end of file diff --git a/lib/Rails/Application/Base.php b/lib/Rails/Application/Base.php new file mode 100755 index 0000000..b714c48 --- /dev/null +++ b/lib/Rails/Application/Base.php @@ -0,0 +1,356 @@ +_run(); + } + + # This is the initializer. + static public function initialize() + { + if (!self::$_instance) { + $cn = get_called_class(); + self::$_instance = new $cn(); + self::$_instance->name = substr($cn, 0, strpos($cn, '\\')); + } + self::$_instance->boot(); + + if (Rails::cli()) + Rails::console()->run(); + } + + static public function instance() + { + return self::$_instance; + } + + static public function configure(Closure $config) + { + $config(self::$_instance->config); + } + + static public function routes() + { + return self::$_instance->_dispatcher->router()->routes(); + } + + # Used only by Rails + static public function setPanelConfig() + { + $basePath = Rails::path() . '/Panel'; + $paths = Rails::config()->paths; + + $paths->application->setPath($basePath)->setBasePaths([]); + $trait = $basePath . '/traits/AdminController.php'; + require_once $trait; + + $trait = $basePath . '/traits/ApplicationController.php'; + require_once $trait; + + Rails::loader()->addPaths([ + $paths->helpers->toString(), + $paths->controllers->toString() + ]); + + Rails::config()->action_view->layout = 'application'; + } + + public function __construct() + { + } + + public function boot() + { + $this->resetConfig(Rails::env()); + $this->setPhpConfig(); + $this->_load_files(); + $this->_load_active_record(); + $this->initPlugins(); + $this->setDispatcher(); + $this->init(); + $this->runInitializers(); + } + + public function resetConfig($environment) + { + $this->setDefaultConfig(); + $this->initConfig($this->config); + $this->setEnvironmentConfig($environment); + } + + public function config() + { + return $this->config; + } + + public function dispatcher() + { + return $this->_dispatcher; + } + + public function controller() + { + return $this->_controller; + } + + /** + * Created so it can be called by Rails when creating the ExceptionHandler + */ + public function set_controller(\Rails\ActionController\Base $controller) + { + $this->_controller = $controller; + } + + public function I18n() + { + return Rails::services()->get('i18n'); + // if (!$this->_I18n) { + // $this->_I18n = new I18n(); + // } + // return $this->_I18n; + } + + public function name() + { + return $this->name; + } + + public function validateSafeIps() + { + return $this->config()->safe_ips->includes($this->dispatcher()->request()->remoteIp()); + } + + public function router() + { + return $this->_dispatcher->router(); + } + + /** + * For custom init. + */ + protected function init() + { + } + + protected function _run() + { + ActionView::clean_buffers(); + ob_start(); + + $this->_dispatcher->find_route(); + if ($this->dispatcher()->router()->route()->assets_route()) { + Rails::assets()->server()->dispatch_request(); + } else { + if ($this->dispatcher()->router()->route()->rails_admin()) + $this->setPanelConfig(); + $this->_load_controller(); + $this->controller()->run_request_action(); + } + + $this->_dispatcher->respond(); + } + + public function setDispatcher() + { + $this->_dispatcher = new ActionDispatch(); + $this->_dispatcher->init(); + } + + /** + * Used in /config/application.php to initialize custom configuration. + * + * @see _default_config() + * @return array. + */ + protected function initConfig($config) + { + } + + private function initPlugins() + { + if ($this->config()->plugins) { + foreach ($this->config()->plugins->toArray() as $pluginName) { + $initClassName = $pluginName . '\Initializer'; + $initializer = new $initClassName(); + $initializer->initialize(); + } + } + } + + private function _load_controller() + { + $route = $this->dispatcher()->router()->route(); + $controller_name = $route->controller(); + + if ($route->namespaces()) { + $namespaces = []; + foreach ($route->namespaces() as $ns) + $namespaces[] = Rails::services()->get('inflector')->camelize($ns); + $class_name = implode('\\', $namespaces) . '\\' . Rails::services()->get('inflector')->camelize($controller_name) . 'Controller'; + } else { + $class_name = Rails::services()->get('inflector')->camelize($controller_name) . 'Controller'; + } + + $controller = new $class_name(); + + if (!$controller instanceof \Rails\ActionController\Base) { + throw new Exception\RuntimeException( + sprintf('Controller %s must be extension of Rails\ActionController\Base', $class_name) + ); + } + $this->_controller = $controller; + } + + /** + * Load default files. + */ + private function _load_files() + { + foreach ($this->config()->load_files as $file) { + require $file; + } + } + + private function _load_active_record() + { + $basename = Rails::root() . '/config/database'; + $database_file = $basename . '.yml'; + $connections = []; + + /** + * It's possible to not to have a database file if no database will be used. + */ + if (is_file($database_file)) { + $connections = Rails\Yaml\Parser::readFile($database_file); + } else { + $database_file = Rails::root() . '/config/database.php'; + if (is_file($database_file)) + $connections = require $database_file; + } + + if ($connections) { + foreach ($connections as $name => $config) + ActiveRecord::addConnection($config, $name); + # Set environment connection. + ActiveRecord::set_environment_connection(Rails::env()); + } + } + + private function setDefaultConfig() + { + $config = Rails::config(); + $config->session = [ + 'name' => '_' . Rails::services()->get('inflector')->underscore($this->name) . '_session_id', + 'start' => true + ]; + $this->config = $config; + } + + private function setEnvironmentConfig($environment) + { + $config = Rails::config(); + + $file = $config->paths->config->concat('environments') . '/' . $environment . '.php'; + if (!is_file($file)) { + throw new Exception\RuntimeException( + sprintf("Environment config file not found [ environment => %s, file => %s ]", + $environment, $file) + ); + } + include $file; + } + + /** + * Sets some php-related config. This happens once (i.e. if the + * config is reset, this method isn't called again). + */ + private function setPhpConfig() + { + if (function_exists('ini_set')) { + if ($this->config->error->log) { + ini_set('log_errors', true); + ini_set('error_log', Rails::config()->log_file); + } + } + + if ($this->config['date']['timezone']) { + date_default_timezone_set($this->config['date']['timezone']); + } elseif (function_exists('ini_get')) { + if (!ini_get('date.timezone') && !date_default_timezone_get()) { + throw new Exception\RuntimeException( + "date.timezone directive doesn't seem to have a value. " . + "You are requested to either set a value in your php.ini or in your application's configuration (date->timezone)" + ); + } + } + + if ($this->config()->session->start && !Rails::cli()) { + if ($session_name = $this->config()->session->name) { + session_name($session_name); + } + session_start(); + } + + if ($paths = $this->config()->eager_load_paths->toArray()) { + Rails::loader()->addPaths($paths); + set_include_path(get_include_path() . PATH_SEPARATOR . implode(PATH_SEPARATOR, $paths)); + } + + set_error_handler('Rails::errorHandler', $this->config()->error->report_types); + } + + private function runInitializers() + { + $dirName = 'initializers'; + $path = $this->config()->paths->config->concat($dirName); + if (is_dir($path)) { + $files = []; + + $patt = $path . '/*.php'; + $files = array_merge($files, glob($patt)); + + foreach (Toolbox\FileTools::listDirs($path) as $dir) { + $patt = $dir . '/*.php'; + $files = array_merge($files, glob($patt)); + } + + foreach ($files as $file) { + require $file; + } + } + } +} \ No newline at end of file diff --git a/lib/Rails/Application/Exception/ExceptionInterface.php b/lib/Rails/Application/Exception/ExceptionInterface.php new file mode 100755 index 0000000..9cf9ff0 --- /dev/null +++ b/lib/Rails/Application/Exception/ExceptionInterface.php @@ -0,0 +1,6 @@ +_get_array()[] = $value; + else + $this->_get_array()[$offset] = $value; + } + + public function offsetExists($offset) + { + return isset($this->_get_array()[$offset]); + } + + public function offsetUnset($offset) + { + unset($this->_get_array()[$offset]); + } + + public function offsetGet($offset) + { + if ($this->offsetExists($offset)) + return $this->_get_array()[$offset]; + return null; + } + + public function getIterator() + { + return new ArrayIterator($this->_get_array()); + } + + public function merge() + { + foreach (func_get_args() as $arr) { + if (!is_array($arr)) + throw new Exception\InvalidArgumentException( + sprintf("All arguments passed to merge() must be array, %s passed", gettype($arr)) + ); + foreach ($arr as $k => $v) + $this->offsetSet($k, $v); + } + return $this; + } +} \ No newline at end of file diff --git a/lib/Rails/ArrayHelper/Exception/ExceptionInterface.php b/lib/Rails/ArrayHelper/Exception/ExceptionInterface.php new file mode 100755 index 0000000..ded69c4 --- /dev/null +++ b/lib/Rails/ArrayHelper/Exception/ExceptionInterface.php @@ -0,0 +1,6 @@ +_var_name = $var_name; + $this->_key_name = $key_name; + $this->merge($values); + } + + public function offsetSet($offset, $value) + { + global ${$this->_var_name}; + if ($offset === null) + ${$this->_var_name}[$this->_key_name][] = $value; + else + ${$this->_var_name}[$this->_key_name][$offset] = $value; + } + + protected function _get_array() + { + global ${$this->_var_name}; + return ${$this->_var_name}; + } +} \ No newline at end of file diff --git a/lib/Rails/Assets/Assets.php b/lib/Rails/Assets/Assets.php new file mode 100755 index 0000000..a77d297 --- /dev/null +++ b/lib/Rails/Assets/Assets.php @@ -0,0 +1,522 @@ +console) { + $this->console->write($message); + } + } + + public function setConsole(Rails\Console\Console $console) + { + $this->console = $console; + } + + public function addPaths($paths) + { + if (!is_array($paths)) { + $paths = [$paths]; + } + + $this->paths = array_merge($this->paths, $paths); + } + + /** + * URL for non-static assets. + */ + public function getFileUrl($filename) + { + if ($file = $this->findFile($filename)) { + return $file->url(); + } + return false; + } + + /** + * Returns not only the URL for the file, but also the URLs + * of the files it requires (if it's a manifest file). + */ + public function getFileUrls($filename) + { + if ($file = $this->findFile($filename)) { + $parser = new Rails\Assets\Parser\Base($file); + $parser->parse(Rails\Assets\Parser\Base::PARSE_TYPE_GET_PATHS); + $urls = []; + foreach ($parser->urlPaths() as $url) { + $urls[] = $url . '?body=1'; + } + return $urls; + } + return false; + } + + public function findFile($filename) + { + $pinfo = pathinfo($filename); + $extension = $pinfo['extension']; + $dir = $pinfo['dirname'] == '.' ? '' : $pinfo['dirname'] . '/'; + $file_relative_path_root = $dir . $pinfo['filename']; + + $found = false; + $pattern = $file_relative_path_root . '.' . $extension . '*'; + + foreach ($this->paths as $assets_root) { + $filePatt = $assets_root . '/' . $pattern; + $files = glob($filePatt); + + if ($files) { + $file = new File($extension, $files[0]); + $found = true; + break; + } + } + + if ($found) { + return $file; + } else { + return false; + } + } + + public function findCompiledFile($file) + { + if (!$this->config()->digest) { + return $this->base_path() . ltrim($this->prefix()) . '/' . $file; + } else { + $index = $this->getManifestIndex(); + + if (isset($index[$file])) { + return $this->base_path() . ltrim($this->prefix()) . '/' . $index[$file]; + } + } + + return false; + } + + /** + * Finds all files named $filename within all assets + * dirs and subdirs. + */ + public function findAllFiles($filename) + { + $pinfo = pathinfo($filename); + $extension = $pinfo['extension']; + $dir = $pinfo['dirname'] == '.' ? '' : $pinfo['dirname'] . '/'; + $file_relative_path_root = $dir . $pinfo['filename']; + + $foundFiles = []; + + $pattern = $file_relative_path_root . '.' . $extension . '*'; + + foreach ($this->paths as $assetsRoot) { + $foundFiles = array_merge($foundFiles, Rails\Toolbox\FileTools::searchFile($assetsRoot, $pattern)); + } + + return $foundFiles; + } + + /** + * Deletes all compiled files and compiles them again. + */ + public function compileAll() + { + $this->emptyCompileDir(); + + $this->compileOtherFiles(); + + $manifestNames = $this->config()->precompile; + + foreach ($manifestNames as $manifest) { + $ext = pathinfo($manifest, PATHINFO_EXTENSION); + $paths = $this->findAllFiles($manifest); + foreach ($paths as $path) { + $this->compileFile(new File($ext, $path)); + } + } + } + + public function addFilePatterns($exts) + { + $this->filePatterns = array_merge($this->filePatterns, (array)$exts); + } + + protected function compileOtherFiles() + { + $exts = $this->filePatterns; + $pattern = '*.{' . implode(',', $exts) .'}'; + $foundFiles = []; + + foreach ($this->paths as $assetsRoot) { + $foundFiles = array_merge($foundFiles, Rails\Toolbox\FileTools::searchFile($assetsRoot, $pattern, GLOB_BRACE)); + } + + $files = []; + $assetsPath = $this->compilePath() . $this->prefix(); + foreach ($foundFiles as $foundFile) { + $file = new File($foundFile); + + $contents = file_get_contents($foundFile); + $this->createCompiledFile($assetsPath . '/' . $file->relative_path(), $contents, false); + + $md5 = md5_file($foundFile); + $md5File = $file->relative_file_root_path() . '-' . $md5 . '.' . $file->type(); + $this->createCompiledFile($assetsPath . '/' . $md5File, $contents, false); + } + } + + /** + * Deletes everything inside the compile folder. + */ + public function emptyCompileDir() + { + $this->console("Deleting compiled assets"); + $dir = $this->compilePath() . $this->prefix(); + if (is_dir($dir)) { + Rails\Toolbox\FileTools::emptyDir($dir); + } + } + + /** + * Accepts: + * string - a filename (e.g. application.css), or + * File object + * + * Compiles, minifies and gz-compresses files. Also updates the + * manifest index file. + * Note that files with same name will be deleted. + */ + public function compileFile($filename) + { + if (!$this->compilePath()) { + throw new Exception\RuntimeException( + "Missing asset configuration 'compile_path'" + ); + } elseif (!$this->manifestFileName) { + throw new Exception\RuntimeException( + sprintf("Property %s::\$manifestFileName must not be empty", __CLASS__) + ); + } + + if (is_string($filename)) { + $file = $this->findFile($filename); + if (!$file) { + throw new Exception\RuntimeException( + sprintf("Asset file not found: %s", $filename) + ); + } + } elseif ($filename instanceof File) { + $file = $filename; + } else { + throw new Exception\InvalidArgumentException( + sprintf( + "Argument must be string or Rails\Assets\File, %s passed", + gettype($filename) + ) + ); + } + + $this->console("Compiling file " . $file->full_path()); + + $basePath = $this->compilePath() . $this->prefix(); + $ext = $file->type(); + $relativeDir = $file->relative_dir(); + if ($relativeDir) { + $relativeDir .= '/'; + } + $fileroot = $basePath . '/' . $relativeDir . $file->file_root(); + + $compiledFileDir = dirname($fileroot); + + if (!is_dir($compiledFileDir)) { + try { + mkdir($compiledFileDir, 0755, true); + } catch (\Exception $e) { + throw new Exception\RuntimeException( + sprintf("Couldn't create dir %s: %s", $compiledFileDir, $e->getMessage()) + ); + } + } + + set_time_limit(360); + + $parser = new Parser\Base($file); + $parser->parse(Parser\Base::PARSE_TYPE_FULL); + $fileContents = $parser->parsed_file(); + + if ($this->config()->compress) { + $fileContents = $this->compressFile($fileContents, $ext); + } + + $compileFiles = [ $fileroot . '.' . $ext ]; + + if ($this->config()->digest) { + $md5 = md5($fileContents); + $compileFiles[] = $fileroot . '-' . $md5 . '.' . $ext; + } + + foreach ($compileFiles as $compileFile) { + $this->createCompiledFile($compileFile, $fileContents); + } + + $relativePath = $relativeDir . $file->file_root(); + + if ($this->config()->digest) { + # Delete previous md5 files + $pattern = $fileroot . '-*.' . $ext . '*'; + if ($mfiles = glob($pattern)) { + $regexp = '/-' . $md5 . '\.' . $ext . '(\.gz)?$/'; + foreach ($mfiles as $mfile) { + if (!preg_match($regexp, $mfile)) { + unlink($mfile); + } + } + } + $this->updateManifestIndex($relativePath . '.' . $ext, $relativePath . '-' . $md5 . '.' . $ext); + } + return true; + } + + protected function createCompiledFile($compiledFilename, $fileContents, $compress = null) + { + $dir = dirname($compiledFilename); + if (!is_dir($dir)) { + mkdir($dir, 0777, true); + } + + if (file_put_contents($compiledFilename, $fileContents)) { + $this->console("Created " . $compiledFilename); + } else { + throw new Exception\RuntimeException( + sprintf("Couldn't write file %s", $compiledFilename) + ); + } + + if (null === $compress) { + $compress = $this->config()->gz_compression; + } + + # Compress files. + if ($compress) { + $gzFile = $compiledFilename . '.gz'; + $gzdata = gzencode($fileContents, $this->config()->gz_compression_level); + + if (file_put_contents($gzFile, $gzdata)) { + $this->console("Created " . $gzFile); + } else { + throw new Exception\RuntimeException( + sprintf("Couldn't write file %s", $compiledFilename) + ); + } + } + } + + /** + * Path where the folder named after $prefix will + * be created to store compiled assets. + */ + public function compilePath() + { + return $this->config()->compile_path; + } + + /** + * Gets the contents of the manifest index file. + * + * @return array + */ + public function getManifestIndex() + { + $file = $this->manifestIndexFile(); + if (is_file($file)) { + $index = Rails\Yaml\Parser::readFile($file); + # Force array. + if (!is_array($index)) { + $index = []; + } + } else { + $index = []; + } + return $index; + } + + /** + * Path to the manifest file. + * + * @return string + */ + public function manifestIndexFile() + { + $basePath = $this->manifestFilePath ?: $this->config()->compile_path . $this->prefix(); + $indexFile = $basePath . '/' . $this->manifestFileName . '.yml'; + return $indexFile; + } + + /** + * @param string $file + * @param string $md5File + */ + protected function updateManifestIndex($file, $md5File) + { + $index = $this->getManifestIndex(); + $index[$file] = $md5File; + Rails\Yaml\Parser::writeFile($this->manifestIndexFile(), $index); + } + + public function prefix() + { + if (!$this->prefix) { + $this->prefix = str_replace('\\', '/', Rails::application()->config()->assets->prefix); + } + return $this->prefix; + } + + public function public_path() + { + return Rails::publicPath() . $this->prefix(); + } + + public function trim_paths(array $files) + { + $trimd = []; + foreach ($files as $file) { + foreach ($this->paths as $path) { + if (is_int(strpos($file, $path))) { + $trimd[] = str_replace($path, '', $file); + continue 2; + } + } + $trimd[] = $file; + } + return $trimd; + } + + public function compressFile($contents, $ext) + { + $key = $ext . '_compressor'; + $conf = $this->config()->$key; + + if (!$conf) { + throw new Exception\RuntimeException( + "No compressor defined for extension $ext" + ); + } + + if ($conf instanceof Closure) { + $compressed = $conf($contents); + } else { + $class = $conf['class_name']; + + $method = $conf['method']; + $static = empty($conf['static']); + + if (!empty($conf['file'])) { + require_once $conf['file']; + } + + if ($static) { + $compressed = $class::$method($contents); + } else { + $compressor = new $class(); + $compressed = $compressor->$method($contents); + } + } + + return $compressed; + } + + public function config() + { + return Rails::application()->config()->assets; + } + + public function cache_read($key) + { + return Rails::cache()->read($key, ['path' => 'rails']); + } + + public function cache_write($key, $contents) + { + return Rails::cache()->write($key, $contents, ['path' => 'rails']); + } + + public function base_path() + { + return Rails::application()->router()->basePath(); + } + + public function paths() + { + return $this->paths; + } + + public function server() + { + if (!$this->server) { + $this->server = new Server; + } + return $this->server; + } +} \ No newline at end of file diff --git a/lib/Rails/Assets/Compiler.php b/lib/Rails/Assets/Compiler.php new file mode 100755 index 0000000..eff79bd --- /dev/null +++ b/lib/Rails/Assets/Compiler.php @@ -0,0 +1,9 @@ +type = $this->extension = $type; + + if (preg_match('/^\w:/', $path) || strpos($path, '/') === 0) { + $path = str_replace('\\', '/', $path); + foreach ($this->assets_paths() as $asset_path) { + if (strpos($path, $asset_path) === 0) { + $this->baseDir = $asset_path; + $path = substr($path, strlen($asset_path) + 1); + break; + } + } + } + + $subDirs = explode('/', str_replace('\\', '/', dirname($path))); + + $parseExt = $path; + + while ($ext != $this->type) { + $this->extensions[] = $ext; + $parseExt = substr($parseExt, 0, (strlen($ext) + 1) * -1); + $ext = pathinfo($parseExt, PATHINFO_EXTENSION); + if (!$ext) { + throw new Exception\InvalidArgumentException( + sprintf( + "Asset of type %s doesn't have the corresponding extension: %s", + $this->type, + $path + ) + ); + } + } + + $fileRoot = pathinfo($parseExt, PATHINFO_FILENAME); + + $this->subDirs = $subDirs; + $this->fileRoot = $fileRoot; + + $this->validate_subdirs(); + } + + public function base_dir($baseDir = null) + { + if ($baseDir) + $this->baseDir = str_replace('\\', '/', $baseDir); + else + return $this->baseDir; + } + + /** + * Relative dir (relative to assets paths). + */ + public function relative_dir() + { + return implode('/', $this->subDirs); + } + + public function full_dir() + { + $relativeDir = $this->relative_dir(); + if ($relativeDir) { + $relativeDir = '/' . $relativeDir; + } + return $this->baseDir . $relativeDir; + } + + public function relative_path() + { + $full_path = implode('/', $this->subDirs) . '/' . $this->fileRoot . '.' . $this->extension; + if ($this->extensions) { + $full_path .= '.' . implode('.', array_reverse($this->extensions)); + } + return ltrim($full_path, '/'); + } + + public function full_path() + { + return $this->baseDir . '/' . $this->relative_path(); + } + + /** + * Full path except file extension + */ + public function full_file_root_path() + { + $subDirs = implode('/', $this->subDirs); + if ($subDirs) { + $subDirs .= '/'; + } + return $this->baseDir . '/' . $subDirs . $this->fileRoot; + } + + /** + * Sub dirs + file root + */ + public function relative_file_root_path() + { + $subDirs = implode('/', $this->subDirs); + if ($subDirs) { + $subDirs .= '/'; + } + return $subDirs . $this->fileRoot; + } + + public function sub_dirs() + { + return $this->subDirs; + } + + public function file_root() + { + return $this->fileRoot; + } + + public function extension() + { + return $this->extension; + } + + public function extensions() + { + return $this->extensions; + } + + public function type() + { + return $this->type; + } + + /** + * Creates a file "familiar": another asset file of the same type, + * whose baseDir is unknown. + */ + public function familiar($file_path) + { + $dirs = explode('/', $file_path); + $new_file_root = array_pop($dirs); + array_unshift($dirs, reset($this->subDirs)); + + $file = new static($this->type); + $file->subDirs = $dirs; + $file->fileRoot = $new_file_root; + $file->extension = $this->extension; + return $file; + } + + public function url($basePath = null, $prefix = null) + { + if (null === $basePath) { + $basePath = Rails::assets()->base_path(); + } + if (null === $prefix) { + $prefix = Rails::assets()->prefix(); + } + + return $basePath . $prefix . '/' . $this->relative_file_root_path() . '.' . $this->type; + } + + protected function assets_paths() + { + return Rails::assets()->paths(); + } + + protected function validate_subdirs() + { + $subDirs = []; + + foreach ($this->subDirs as $sd) { + if (preg_match('/^[\w-]+$/', $sd)) + $subDirs[] = $sd; + } + + $this->subDirs = $subDirs; + } +} \ No newline at end of file diff --git a/lib/Rails/Assets/Initializer.php b/lib/Rails/Assets/Initializer.php new file mode 100755 index 0000000..be00dca --- /dev/null +++ b/lib/Rails/Assets/Initializer.php @@ -0,0 +1,15 @@ +parseType = $parseType; + + $this->getFileContents(); + + if ($this->parseType == self::PARSE_TYPE_NONE) { + $this->parsedFile = $this->fileContents; + $this->fileContents = null; + } else { + if ($this->parseType == self::PARSE_TYPE_GET_PATHS) { + $this->urlPaths[] = $this->file->url(); + } + $this->extractDirectives(); + $this->executeDirectives(); + } + + return $this; + } + + /** + * $path_parts = [0]=>assets_path; [1]=>filename; [2]=>extension; + */ + public function __construct($file, Base $parentParser = null) + { + if (!$file instanceof File) { + $extension = pathinfo($file, PATHINFO_EXTENSION); + $file = new File($extension, $file); + } + + $this->file = $file; + + if (!isset(self::$_first_parents[$file->extension()])) { + self::$_first_parents[$file->extension()] = $this; + } + + $this->parentParser = $parentParser; + + $this->firstParent()->add_required_file($file); + } + + protected function extractDirectives() + { + $lines = explode("\n", $this->fileContents); + + $commentBlock = false; + + $directivesRegexp = implode('|', [ + 'require_self', + 'require_directory [.-\w\s\/]+', + 'require_tree [.-\w\s\/]+', + 'require [.-\w\s\/]+', + 'include [.-\w\s\/]+', + ]); + + foreach ($lines as $k => $line) { + $line = trim($line); + + if (!$line) { + continue; + } + + $ab = substr($line, 0, 2); + $c = substr($line, 2, 1); + + if ($ab == '/*') { + $commentBlock = true; + } elseif (!$commentBlock && !preg_match('~^\s*//|/\*~', $line)) { + # Not a comment and not in a comment block. + # Directives block ended. + break; + } + + if (preg_match('~^\W+=\s+?(' . $directivesRegexp . ')~', $line, $m)) { + $this->directives[] = array_filter(explode(' ', $m[1])); + unset($lines[$k]); + } + + if ($commentBlock && is_int(strpos($line, '*/'))) { + $commentBlock = false; + } + } + + $this->fileContents = implode("\n", $lines); + } + + protected function executeDirectives() + { + if ($this->directives) { + foreach ($this->directives as $directive) { + $command = $directive[0]; + + if (isset($directive[1])) { + $params = $directive[1]; + } + + switch ($command) { + case 'require_directory': + $this->requireDir($params); + break; + + case 'require_tree': + $this->requireTree($params); + break; + + case 'require_self': + $this->requireSelf(); + break; + + case 'require': + $this->requireFile($params . '.' . $this->file->type()); + break; + + case 'include': + $this->includeFile($params . '.' . $this->file->type()); + break; + + default: + throw new Exception\RuntimeException( + sprintf("Invalid directive in file %s:\n%s", $this->file->full_path(), var_export($directive, true)) + ); + } + } + } + + $this->requireSelf(); + $this->parsedFile = implode("\n", $this->parsedFile); + } + + public function urlPaths() + { + return $this->urlPaths; + } + + protected function requireSelf() + { + if (!$this->requiredSelf) { + $this->requiredSelf = true; + $this->parsedFile[] = $this->fileContents; + $this->fileContents = null; + } + } + + protected function includeFile($filename) + { + if (!$file = $this->filenameToFile($filename)) { + throw new Exception\FileNotFoundException( + sprintf( + "Included file '%s' not found. Require trace:\n%s", + $filename, + var_export($this->_parents_trace(), true) + ) + ); + } + + $parser = new static($file, $this); + $parser->parse($this->parseType); + + if ($this->parseType == Base::PARSE_TYPE_GET_PATHS) { + $this->urlPaths = array_merge($this->urlPaths, $parser->urlPaths()); + } + + $this->firstParent()->add_required_file($file); + $this->parsedFile[] = $parser->parsed_file(); + } + + protected function requireFile($filename) + { + if (!$file = $this->filenameToFile($filename)) { + throw new Exception\FileNotFoundException( + sprintf( + "Required file '%s' not found. Require trace:\n%s", + $filename, + var_export($this->_parents_trace(), true) + ) + ); + } + + if ($this->firstParent()->file_already_required($file)) { + return false; + } + + $this->includeFile($file); + } + + private function filenameToFile($filename) + { + if (!$filename instanceof File) { + $file = Rails::assets()->findFile($filename); + if (!$file) { + return false; + } + } else { + $file = $filename; + } + + return $file; + } + + protected function requireDir($dir) + { + $path = $this->file->full_dir() . '/' . $dir; + $realPath = realpath($path); + + if (!$realPath) { + throw new Exception\RuntimeException( + sprintf( + "Directory not found: %s", + $path + ) + ); + } + + $files = glob($realPath . '/*.' . $this->file->type() . '*'); + + foreach ($files as $file) { + $file = new File($this->file->type(), $file); + $this->requireFile($file); + } + } + + protected function requireTree($tree) + { + $rootDir = $this->file->full_dir(); + + $path = $rootDir . '/' . $tree; + $realPath = realpath($path); + + if (!$realPath) { + throw new Exception\RuntimeException( + sprintf( + "Path to tree not found: %s", + $path + ) + ); + } + + # Require files in root directory. + $this->requireDir($tree); + + # Require files in sub directories. + $allDirs = Toolbox\FileTools::listDirs($realPath); + + foreach ($allDirs as $dir) { + $relativeDir = trim(substr($dir, strlen($rootDir)), '/'); + $this->requireDir($relativeDir); + } + } + + public function parsedFile($implode = true) + { + return $implode && is_array($this->parsedFile) ? implode(self::EOL, $this->parsedFile) : $this->parsedFile; + } + + public function parsed_file($implode = true) + { + return $this->parsedFile($implode); + } + + public function parentParser() + { + return $this->parentParser; + } + + public function add_required_file($file) + { + $this->_required_files[] = $file->full_file_root_path(); + } + + public function file_already_required($filename) + { + return in_array($filename->full_file_root_path(), $this->_required_files); + } + + public function required_files() + { + return $this->_required_files; + } + + public function config() + { + return Assets::instance()->config(); + } + + protected function getFileContents() + { + // if ($cached = $this->_cached_other_extension_file()) + // return $cached + + $contents = file_get_contents($this->file->full_path()); + + if ($this->file->extensions()) { + foreach ($this->file->extensions() as $ext) { + if ($ext == 'php') { + $contents = PHParser::parseContents($contents); + // $contents = $parser->parse(); + } else { + $key = $this->file->type() . '_extensions'; + // if (!$this->config()->$key) + // vpe($key); + // vpe($this->config()->$key); + $conf = $this->config()->$key->$ext; + // $conf = $this->config()['parsers'][self::EXTENSION][$ext]; + + if (!$conf) { + throw new Exception\RuntimeException( + sprintf( + "Unknown parser [ file=>%s, extension=>%s ]", + $this->file->full_path(), + $ext + ) + ); + } + + if ($conf instanceof Closure) { + $conf($contents); + } else { + $class = $conf['class_name']; + $method = $conf['method']; + $static = !empty($conf['static']); + + if (!empty($conf['file'])) { + // vpe(get_include_path()); + require_once $conf['file']; + } + + if ($static) { + $func = $class . '::' . $method; + $contents = $func($contents); + } else { + $parser = new $class(); + $contents = $parser->$method($contents); + } + } + } + } + } + + $this->fileContents = $contents; + } + + protected function firstParent() + { + return self::$_first_parents[$this->file->extension()]; + } + + protected function _parents_trace() + { + $parent = $this->parentParser; + if (!$parent) + return [$this->file->full_path()]; + + $files = [$parent->file->full_path()]; + + while ($parent) { + $parent = $parent->parentParser; + $parent && $files[] = $parent->file->full_path(); + } + return $files; + } +} \ No newline at end of file diff --git a/lib/Rails/Assets/Parser/Javascript/ClosureApi/ClosureApi.php b/lib/Rails/Assets/Parser/Javascript/ClosureApi/ClosureApi.php new file mode 100755 index 0000000..4c3d293 --- /dev/null +++ b/lib/Rails/Assets/Parser/Javascript/ClosureApi/ClosureApi.php @@ -0,0 +1,87 @@ +makeRequest($code, ['output_info' => 'errors']); + + if (trim($obj->resp())) { + if (self::$save_file_on_error) + file_put_contents(self::errorFile(), $code); + throw new Exception\ErrorsOnCodeException($obj->resp()); + } + + $obj->makeRequest($code); + $resp = $obj->resp(); + if (!trim($resp)) + throw new Exception\BlankResponseException("Closure returned an empty string (file too large? size => ".strlen($code).")"); + return $resp; + } + + static public function errorFile() + { + if (!self::$save_path) + self::$save_path = Rails::root() . '/tmp'; + + return self::$save_path . '/' . self::$errorFile_name; + } + + public function __construct($params) + { + $this->_params = array_merge($this->defaultParams(), $params); + } + + public function makeRequest($code, array $extra_params = []) + { + $params = array_merge($this->_params, $extra_params); + $params['js_code'] = $code; + + $ch = curl_init(self::API_URL); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => http_build_query($params), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => [ + "Content-type: application/x-www-form-urlencoded" + ] + ]); + $this->_resp = curl_exec($ch); + $this->_lastInfo = curl_getinfo($ch); + curl_close($ch); + } + + public function resp() + { + return $this->_resp; + } + + public function lastInfo() + { + return $this->_lastInfo; + } + + protected function defaultParams() + { + return [ + 'compilation_level' => 'SIMPLE_OPTIMIZATIONS', + 'output_info' => 'compiled_code', + 'output_format' => 'text', + 'language' => 'ECMASCRIPT5' + ]; + } +} \ No newline at end of file diff --git a/lib/Rails/Assets/Parser/Javascript/ClosureApi/Exception/BlankResponseException.php b/lib/Rails/Assets/Parser/Javascript/ClosureApi/Exception/BlankResponseException.php new file mode 100755 index 0000000..c2b8460 --- /dev/null +++ b/lib/Rails/Assets/Parser/Javascript/ClosureApi/Exception/BlankResponseException.php @@ -0,0 +1,6 @@ +parse($contents); + } + + public function __construct($file) + { + $this->file = $file; + } + + public function parse($contents) + { + ob_start(); + eval('?>' . $contents); + return ob_get_clean(); + } + + /** + * Returns relative paths. + */ + protected function assetPath($file, array $options = []) + { + $root = \Rails::application()->router()->rootPath(); + if ($root == '/') { + $root = ''; + } + $path = Rails::assets()->prefix() . '/' . $file; + return $this->getRelativePath($this->file->url(), $path); + } + + # SO: /a/12236654/638668 + protected function getRelativePath($from, $to) + { + $from = str_replace('\\', '/', $from); + $to = str_replace('\\', '/', $to); + + $from = explode('/', $from); + $to = explode('/', $to); + $relPath = $to; + + foreach($from as $depth => $dir) { + if($dir === $to[$depth]) { + array_shift($relPath); + } else { + $remaining = count($from) - $depth; + if($remaining > 1) { + $padLength = (count($relPath) + $remaining - 1) * -1; + $relPath = array_pad($relPath, $padLength, '..'); + break; + } else { + $relPath[0] = './' . $relPath[0]; + } + } + } + return implode('/', $relPath); + } +} diff --git a/lib/Rails/Assets/Parser/PHParser.php b/lib/Rails/Assets/Parser/PHParser.php new file mode 100755 index 0000000..cbbf301 --- /dev/null +++ b/lib/Rails/Assets/Parser/PHParser.php @@ -0,0 +1,26 @@ +parse($contents); + } + + public function parse($contents) + { + ob_start(); + eval('?>' . $contents); + return ob_get_clean(); + } +} diff --git a/lib/Rails/Assets/Server.php b/lib/Rails/Assets/Server.php new file mode 100755 index 0000000..599b9ca --- /dev/null +++ b/lib/Rails/Assets/Server.php @@ -0,0 +1,153 @@ +request()->path(); + + if ($base_path = $this->base_path()) + $path = str_replace($base_path, '', $path); + + $request = str_replace(Rails::assets()->prefix() . '/', '', $path); + $this->serve_file($request); + } + + public function serve_file($file_path) + { + $file = Rails::assets()->findFile($file_path); + + if (!$file) { + $this->set_404_headers(); + return; + } + + $ext = $file->type(); + $parser = null; + + switch ($ext) { + case 'js': + $parser = new Parser\Base($file); + $this->set_javascript_headers(); + break; + + case 'css': + $parser = new Parser\Base($file); + $this->set_stylesheet_headers(); + break; + + case 'jpeg': + $this->headers()->contentType('image/jpeg'); + break; + + case 'jpg': + $this->headers()->contentType('image/jpeg'); + break; + + case 'png': + $this->headers()->contentType('image/png'); + break; + + case 'gif': + $this->headers()->contentType('image/gif'); + break; + + case 'svg': + $this->headers()->contentType('image/svg+xml'); + break; + + case 'ttf': + $this->headers()->contentType('application/x-font-ttf'); + break; + + case 'woff': + $this->headers()->contentType('application/font-woff'); + break; + + default: + $this->headers()->contentType('application/octet-stream'); + return; + } + + if ($parser) { + if ($this->params()->body) { + $parseType = Parser\Base::PARSE_TYPE_NONE; + } else { + $parseType = Parser\Base::PARSE_TYPE_FULL; + } + + $parser->parse($parseType); + $file_contents = $parser->parsed_file(); + } else { + $file_contents = file_get_contents($file->full_path()); + } + + $etag = md5($file_contents); + $date = date('D, d M Y H:i:s e'); + $last_modified = Rails::cache()->fetch('assets.last_mod.' . $file->full_path(), function() use ($date){ + return $date; + }); + + if ($this->file_modified($etag, $last_modified)) { + $this->headers()->add("Last-Modified", $last_modified); + $this->headers()->add("Cache-Control", 'public, max-age=31536000'); + $this->headers()->add("ETag", $etag); + Rails::application()->dispatcher()->response()->body($file_contents); + } else { + $this->headers()->status('HTTP/1.1 304 Not Modified'); + Rails::application()->dispatcher()->response()->body(''); + } + } + + public function base_path() + { + return Rails::application()->router()->basePath(); + } + + public function request() + { + return Rails::application()->dispatcher()->request(); + } + + public function params() + { + return Rails::application()->dispatcher()->parameters(); + } + + public function headers() + { + return Rails::application()->dispatcher()->headers(); + } + + private function file_modified($etag, $last_modified) + { + $if_modified_since = isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) ? $_SERVER['HTTP_IF_MODIFIED_SINCE'] : false; + $if_none_match = isset($_SERVER['HTTP_IF_NONE_MATCH']) ? $_SERVER['HTTP_IF_NONE_MATCH'] : false; + + if ((($if_none_match && $if_none_match == $etag) || !$if_none_match) && + ($if_modified_since && $if_modified_since == $last_modified)) + { + return false; + } else { + return true; + } + } + + private function set_404_headers() + { + $this->headers()->status(404); + } + + private function set_javascript_headers() + { + $this->headers()->contentType('application/javascript'); + } + + private function set_stylesheet_headers() + { + $this->headers()->contentType('text/css'); + } +} \ No newline at end of file diff --git a/lib/Rails/Assets/Traits/AssetPathTrait.php b/lib/Rails/Assets/Traits/AssetPathTrait.php new file mode 100755 index 0000000..a17cc7b --- /dev/null +++ b/lib/Rails/Assets/Traits/AssetPathTrait.php @@ -0,0 +1,31 @@ +assetPath('jquery-ui/loading.gif'); + */ + protected function assetPath($file, array $options = []) + { + if (!isset($options['digest'])) { + $options['digest'] = true; + } + + if ($options['digest']) { + if ($path = \Rails::assets()->findCompiledFile($file)) { + return $path; + } + } + $root = \Rails::application()->router()->rootPath(); + if ($root == '/') { + $root = ''; + } + return $root . \Rails::assets()->prefix() . '/' . $file; + } +} diff --git a/lib/Rails/Cache/Cache.php b/lib/Rails/Cache/Cache.php new file mode 100755 index 0000000..cf6cda1 --- /dev/null +++ b/lib/Rails/Cache/Cache.php @@ -0,0 +1,92 @@ +toArray(); + + switch ($config[0]) { + case 'file_store': + $class = '\Rails\Cache\Store\FileStore'; + break; + + case 'mem_cached_store': + $class = '\Rails\Cache\Store\MemCachedStore'; + break; + + default: + $class = $config[0]; + break; + } + + array_shift($config); + + $this->store = new $class($config); + } + + public function read($key, array $params = []) + { + return $this->store->read($key, $params); + } + + public function write($key, $value, array $options = []) + { + return $this->store->write($key, $value, $options); + } + + public function delete($key, array $params = []) + { + return $this->store->delete($key, $params); + } + + public function exists($key) + { + return $this->store->exists($key); + } + + public function fetch($key, $options = null, Closure $block = null) + { + if ($options instanceof Closure) { + $block = $options; + $options = []; + } + $value = $this->read($key, $options); + + if ($value === null) { + $value = $block(); + $this->write($key, $value, $options); + } + return $value; + } + + public function readMulti() + { + $names = func_get_args(); + if (is_array(end($names))) + $options = array_pop($names); + else + $options = []; + + $results = []; + foreach ($names as $name) { + if (null !== ($value = $this->read($name))) + $results[$name] = $value; + } + return $results; + } + + public function store() + { + return $this->store; + } +} diff --git a/lib/Rails/Cache/Entry.php b/lib/Rails/Cache/Entry.php new file mode 100755 index 0000000..a1a1b6d --- /dev/null +++ b/lib/Rails/Cache/Entry.php @@ -0,0 +1,171 @@ +_key = $key; + $this->_hash = $this->_hash_key($key); + } + + public function read(array $params = []) + { + if (isset($params['path'])) { + $this->_dir = $params['path']; + unset($params['path']); + } + + if ($this->file_exists()) + $this->_read_file(); + return $this->_value; + } + + public function write($val, array $params) + { + $this->_value = serialize($val); + if (isset($params['expires_in'])) { + if (!ctype_digit((string)$params['expires_in'])) + $params['expires_in'] = strtotime($params['expires_in']); + } + if (isset($params['path'])) { + $this->_dir = $params['path']; + unset($params['path']); + } + $this->_params = $params; + + $header = []; + foreach ($params as $k => $v) + $header[] = $k . self::KEY_VALUE_SEPARATOR . $v; + $header = implode(self::DATA_SEPARATOR, $header); + + if (!is_dir($this->_path())) + mkdir($this->_path(), 0777, true); + + file_put_contents($this->_file_name(), $header . "\n" . $this->_value); + } + + public function delete() + { + $this->_value = null; + $this->_delete_file(); + } + + public function value() + { + return $this->_value; + } + + public function file_exists() + { + if ($this->_file_exists === null) { + $this->_file_exists = is_file($this->_file_name()); + } + return $this->_file_exists; + } + + public function unserialize_e_handler() + { + $this->_value = false; + } + + private function _read_file() + { + $this->_file_contents = file_get_contents($this->_file_name()); + $this->_parse_contents(); + if ($this->_expired()) { + $this->delete(); + } else { + + } + } + + private function _parse_contents() + { + $regex = '/^(\V+)/'; + preg_match($regex, $this->_file_contents, $m); + if (!empty($m[1])) { + foreach(explode(self::DATA_SEPARATOR, $m[1]) as $data) { + list($key, $val) = explode(self::KEY_VALUE_SEPARATOR, $data); + $this->_params[$key] = $val; + } + } else + $m[1] = ''; + + # For some reason, try/catch Exception didn't work. + $err_handler = set_error_handler([$this, "unserialize_e_handler"]); + + $this->_value = unserialize(str_replace($m[1] . "\n", '', $this->_file_contents)); + $this->_file_contents = null; + + set_error_handler($err_handler); + } + + private function _expired() + { + if (!isset($this->_params['expires_in']) || $this->_params['expires_in'] > time()) + return false; + return true; + } + + private function _delete_file() + { + if (is_file($this->_file_name())) + unlink($this->_file_name()); + } + + private function _file_name() + { + return $this->_path() . '/' . $this->_hash; + } + + private function _generate_path($key) + { + $md5 = $this->_hash_key($key); + $ab = substr($md5, 0, 2); + $cd = substr($md5, 2, 2); + return $this->_base_path() . '/' . $ab . '/' . $cd; + } + + private function _base_path() + { + $subdir = $this->_dir ? '/' . str_replace('.', '', $this->_dir) : ''; + return Rails::cache()->path() . $subdir; + } + + private function _hash_key($key) + { + return md5($key); + } + + private function _path() + { + if (!$this->_path) { + $this->_path = $this->_generate_path($this->_key); + } + return $this->_path; + } +} \ No newline at end of file diff --git a/lib/Rails/Cache/Exception/ExceptionInterface.php b/lib/Rails/Cache/Exception/ExceptionInterface.php new file mode 100755 index 0000000..2c5c49d --- /dev/null +++ b/lib/Rails/Cache/Exception/ExceptionInterface.php @@ -0,0 +1,6 @@ +basePath = $config[0]; + } + + public function read($key, array $params = []) + { + return $this->getEntry($key, $params)->read(); + } + + public function write($key, $value, array $params) + { + return $this->getEntry($key, $params)->write($value); + } + + public function delete($key, array $params) + { + return $this->getEntry($key, $params)->delete(); + } + + public function exists($key, array $params) + { + return $this->getEntry($key, $params)->fileExists(); + } + + /** + * Removes cache files from directory. + */ + public function deleteDirectory($dirname) + { + $dirpath = $this->path() . '/' . $dirname; + + if (is_dir($dirpath)) { + Toolbox::emptyDir($dirpath); + } + } + + public function basePath() + { + return $this->basePath; + } + + protected function getEntry($key, array $params) + { + return new Entry($key, $params, $this); + } +} diff --git a/lib/Rails/Cache/Store/FileStore/Entry.php b/lib/Rails/Cache/Store/FileStore/Entry.php new file mode 100755 index 0000000..2a9c0b5 --- /dev/null +++ b/lib/Rails/Cache/Store/FileStore/Entry.php @@ -0,0 +1,181 @@ +store = $store; + + $this->_key = $key; + $this->_hash = $this->_hash_key($key); + + if (isset($params['path'])) { + $this->_dir = $params['path']; + unset($params['path']); + } + + $this->params = $params; + } + + public function read() + { + if ($this->fileExists()) + $this->_read_file(); + + return $this->_value; + } + + public function write($val) + { + $this->_value = serialize($val); + + if (isset($this->params['expires_in'])) { + if (!ctype_digit((string)$this->params['expires_in'])) + $this->params['expires_in'] = strtotime('+' . $this->params['expires_in']); + } + if (isset($this->params['path'])) { + $this->_dir = $this->params['path']; + unset($this->params['path']); + } + $this->params = $this->params; + + $header = []; + foreach ($this->params as $k => $v) + $header[] = $k . self::KEY_VALUE_SEPARATOR . $v; + $header = implode(self::DATA_SEPARATOR, $header); + + if (!is_dir($this->_path())) + mkdir($this->_path(), 0777, true); + + return (bool)file_put_contents($this->_file_name(), $header . "\n" . $this->_value); + } + + public function delete() + { + $this->_value = null; + return $this->_delete_file(); + } + + public function value() + { + return $this->_value; + } + + public function fileExists() + { + if ($this->_file_exists === null) { + $this->_file_exists = is_file($this->_file_name()); + } + return $this->_file_exists; + } + + public function unserialize_e_handler() + { + $this->_value = false; + } + + private function _read_file() + { + $this->_file_contents = file_get_contents($this->_file_name()); + $this->_parse_contents(); + if ($this->_expired()) { + $this->delete(); + } else { + + } + } + + private function _parse_contents() + { + $regex = '/^(\V+)/'; + preg_match($regex, $this->_file_contents, $m); + if (!empty($m[1])) { + foreach(explode(self::DATA_SEPARATOR, $m[1]) as $data) { + list($key, $val) = explode(self::KEY_VALUE_SEPARATOR, $data); + $this->params[$key] = $val; + } + } else + $m[1] = ''; + + # For some reason, try/catch Exception didn't work. + $err_handler = set_error_handler([$this, "unserialize_e_handler"]); + + $this->_value = unserialize(str_replace($m[1] . "\n", '', $this->_file_contents)); + $this->_file_contents = null; + + set_error_handler($err_handler); + } + + private function _expired() + { + if (!isset($this->params['expires_in']) || $this->params['expires_in'] > time()) + return false; + return true; + } + + private function _delete_file() + { + if (is_file($this->_file_name())) + return unlink($this->_file_name()); + return true; + } + + private function _file_name() + { + return $this->_path() . '/' . $this->_hash; + } + + private function _hash_key($key) + { + return md5($key); + } + + private function _path() + { + if (!$this->_path) { + $this->_path = $this->_generate_path($this->_key); + } + return $this->_path; + } + + private function _generate_path($key) + { + $md5 = $this->_hash_key($key); + $ab = substr($md5, 0, 2); + $cd = substr($md5, 2, 2); + return $this->_base_path() . '/' . $ab . '/' . $cd; + } + + private function _base_path() + { + $subdir = $this->_dir ? '/' . str_replace('.', '', $this->_dir) : ''; + return $this->store->basePath() . $subdir; + } +} \ No newline at end of file diff --git a/lib/Rails/Cache/Store/MemCachedStore.php b/lib/Rails/Cache/Store/MemCachedStore.php new file mode 100755 index 0000000..a66e99e --- /dev/null +++ b/lib/Rails/Cache/Store/MemCachedStore.php @@ -0,0 +1,71 @@ +connection = new Memcached('Rails.Application.' . Rails::application()->name()); + $this->connection->addServers($servers); + } + + public function connection() + { + return $this->connection; + } + + public function read($key, array $params = []) + { + $value = $this->connection->get($key); + + if (!$value) { + if ($this->connection->getResultCode() == Memcached::RES_NOTFOUND) { + return null; + } else { + # There was some kind of error. + } + } else { + return unserialize($value); + } + } + + public function write($key, $val, array $params) + { + $val = serialize($val); + + if (isset($params['expires_in'])) { + if (!ctype_digit((string)$params['expires_in'])) + $expires_in = strtotime('+' . $params['expires_in']); + } else + $expires_in = 0; + + $resp = $this->connection->add($key, $val, $expires_in); + } + + public function delete($key, array $params) + { + $time = !empty($params['time']) ? $params['time'] : 0; + return $this->connection->delete($key, $time); + } + + public function exists($key, array $params) + { + if ($this->connection->get($key)) + return true; + else { + # An error could have occured. + return false; + } + } +} \ No newline at end of file diff --git a/lib/Rails/Config/Config.php b/lib/Rails/Config/Config.php new file mode 100755 index 0000000..9a36314 --- /dev/null +++ b/lib/Rails/Config/Config.php @@ -0,0 +1,88 @@ +container); + } + + public function offsetSet($offset, $value) + { + if (null === $offset) { + $this->container[] = $value; + } else { + if (is_array($value)) { + $value = new self($value); + } + $this->container[$offset] = $value; + } + } + public function offsetExists($offset) + { + return isset($this->container[$offset]); + } + public function offsetUnset($offset) + { + unset($this->container[$offset]); + } + public function offsetGet($offset) + { + return isset($this->container[$offset]) ? $this->container[$offset] : null; + } + + public function __construct(array $config = []) + { + $this->add($config); + } + + public function __set($prop, $value) + { + if (is_array($value)) + $this->offsetSet($prop, new self($value)); + else + $this->offsetSet($prop, $value); + } + + public function __get($prop) + { + if ($this->offsetExists($prop)) + return $this->offsetGet($prop); + } + + public function __isset($prop) + { + return (bool)$this->__get($prop); + } + + public function add(array $config) + { + foreach ($config as $name => $value) { + $this->__set($name, $value); + } + } + + public function merge(array $config) + { + $this->container = array_merge($this->container, $config); + return $this; + } + + public function toArray() + { + return $this->container; + } + + public function includes($value) + { + return in_array($value, $this->container); + } + + public function keys() + { + return array_keys($this->container); + } +} \ No newline at end of file diff --git a/lib/Rails/Config/default_config.php b/lib/Rails/Config/default_config.php new file mode 100755 index 0000000..06981e4 --- /dev/null +++ b/lib/Rails/Config/default_config.php @@ -0,0 +1,295 @@ + $appPath, + 'views' => $viewsPath, + 'config' => new Rails\Paths\Path('config', [self::$root]), + 'controllers' => new Rails\Paths\Path('controllers', [$appPath]), + 'helpers' => new Rails\Paths\Path('helpers', [$appPath]), + 'models' => new Rails\Paths\Path('models', [$appPath]), + 'layouts' => new Rails\Paths\Path('layouts', [$viewsPath]), + 'mailers' => new Rails\Paths\Path('mailers', [$appPath]), + 'log' => new Rails\Paths\Path('log', [self::$root]), +]); + +$config = new Rails\Config\Config(); + +$config->paths = $paths; + +$config->base_path = ''; // must NOT include leading nor trailing slash. + +/** + * If names begin with a letter, the path of the file + * will be relative to Rails::root(). + */ +$config->load_files = []; + +$config->rails_panel_path = false; // NO leading nor trailing slashes, e.g. "sysadmin", "foo/bar/sysadmin" + +$config->safe_ips = [ + '127.0.0.1', + '::1' +]; + +$config->error = [ + 'report_types' => E_ALL, + 'log' => true, + 'log_max_size' => 1024 +]; + +$config->consider_all_requests_local = !empty($_SERVER['REMOTE_ADDR']) && $_SERVER['REMOTE_ADDR'] === '127.0.0.1'; + +$config->date = [ + 'timezone' => null +]; + +$config->i18n = [ + 'path' => Rails::root() . '/config/locales', + 'default_locale' => 'en' +]; + +$config->action_controller = [ + 'base' => [ + 'default_charset' => 'utf-8' + ], + 'exception_handler' => false +]; + +# Temporary flag +$config->ar2 = false; + +$config->active_record = [ + 'use_cached_schema' => false, + // 'base' => [ + // /** + // * If true, overloading attributes or associations via __get() won't work + // * on models. This is temporary, as now overloading should be don't through __call(). + // */ + // 'call_only_attr_overload' => false + // ] +]; + +$config->action_view = [ + 'layout' => 'application', + 'suffix' => 'phtml', + 'helpers' => [ + // 'will_paginate' => [ + // /** + // * Sets a default custom renderer class. + // * Built-in renderers are Bootstrap and Legacy. + // * A custom renderer class can be created, extending + // * Rails\ActionView\Helper\WillPaginate\AbstractRenderer. + // */ + // 'renderer' => 'bootstrap', + // 'always_show' => false # Show pagination even if there's only 1 page + // ] + ] +]; + +$config->action_mailer = [ + /** + * Allows "sendmail", "smtp" or "file". + * Also allows a Closure that must return a transport + * implementing Zend\Mail\Transport\TransportInterface + */ + 'delivery_method' => 'sendmail', + + 'smtp_settings' => [ + 'address' => '', + 'port' => 25, + 'domain' => '', + 'user_name' => '', + 'password' => '', + 'authentication' => 'login', + 'enable_starttls_auto' => true // Requires openssl PHP extension. + ], + + 'file_settings' => [ + /** + * Directory where mails will be stored. + * + * Default directory is created automatically, otherwise + * it must already exist. + * + * Defaults to Rails::root() . /tmp/mail. + */ + 'location' => null, + + /** + * Any callable argument. + * + * Defaults to Rails\ActionMailer\ActionMailer::filenameGenerator() + */ + 'name_generator' => null + ], + + 'default_options' => [], +]; + +/** + * If assets are enabled, this option will make Assets serve static assets from the public folder + * or non-static from application's folders. + */ +$config->serve_static_assets = false; + +$config->assets = [ + 'enabled' => true, + + /** + * In development, files included by manifests are served individually to help debug javascript. + * However, including many files can increase the page load times a lot. + * With this option, manifest files and their children are concatenated, resulting in 1 file. + */ + 'concat' => false, + + /** + * Additional [absolute] paths to search assets in. + */ + 'paths' => [], + + /** + * Defines how an extension is handled. + * Pass either an array or a Closure. + * + * The Closure must expect 1 parameter which are the contents + * of the file to be processed. + * + * Arrays accept 4 parameters: + * file: if present, the file will be require()'d + * class_name: the name of the class. + * method: the name of the method to which the contents of the file + * will be passed. + * static: if true, the method will be called statically. + */ + 'css_extensions' => [ + 'scss' => [ + 'file' => 'scss.inc.php', + 'class_name' => 'scssc', + 'method' => 'compile', + 'static' => false + ] + ], + + 'js_extensions' => [ + // CoffeeScript + ], + + /** + * This is Not zip compression. + * This tells Assets to minify files upon compile using the compressors + * defined below. + */ + 'compress' => true, + + /** + * Compressors. + * Accepts an array like the one for extensions. + */ + 'css_compressor' => [ + 'class_name' => 'Minify_CSS_Compressor', + 'method' => 'process' + ], + 'js_compressor' => [ + 'class_name' => 'Rails\Assets\Parser\Javascript\ClosureApi\ClosureApi', + 'method' => 'minify' + ], + + /** + * Create a gzipped version of compiled files. + * Uses gzencode() + */ + 'gz_compression' => true, + + 'gz_compression_level' => 9, + + /** + * Names of the manifest files that will be compiled upon + * Assets::compileAll(). Only names, no paths. + */ + 'precompile' => [ + 'application.js', + 'application.css' + ], + + /** + * Non js or css files that will be copied to the public assets folder + * when compiling. These values will be passed to glob() with GLOB_BRACE. + */ + 'patterns' => [ + '.gif', + '.png', + '.jpg', + '.jpeg', + ], + + /** + * This is used as the URL path to server non-static assets for + * development, and is also the name of the folder where assets are + * stored. + */ + 'prefix' => '/assets', + + /** + * Path where the prefix folder will be created, and inside it, compiled + * assets will finally be stored. + */ + 'compile_path' => self::$publicPath, + + # Generate digests for assets URLs + 'digest' => false, + + # Has no use. + 'version' => '1.0' +]; + +$config->cookies = [ + 'expires' => 0, + 'path' => null, # Defaults to base_path() . '/', set in Cookies + 'domain' => null, + 'secure' => false, + 'httponly' => false, + 'raw' => false +]; + +$config->eager_load_paths = []; + +/** + * Set's the cache storing class. + * + * For FileStore + * cache_store ( string 'file_store', string $cache_path ) + * + * For Memcached + * cache_store ( string 'memcached' [, mixed $server1 [, mixed $server2 ... ] ] ) + * Defaults to host "localhost" and port 11211. + * Examples: + * Add 1 server with default options. + * $config->cache_store = [ 'memcached' ]; + * Add 1 server with foobarhost as host and default port. + * $config->cache_store = [ 'memcached', 'foobarhost' ]; + * Add 2 servers: one with localhost as host with default port, and the other one + * with other.server as host and 4456 as port. + * $config->cache_store = [ 'memcached', 'localhost', ['other.server', 4456] ]; + */ +$config->cache_store = ['file_store', Rails::root() . '/tmp/cache']; + +/** + * This is supposed to be used to set a custom logger. + * But rather, customize the default logger in Application\Base::init(). + */ +$config->logger = null; + +$config->log_file = Rails::root() . '/log/' . Rails::env() . '.log'; + +/** + * Accepts int from 0 to 8. + */ +$config->log_level = false; + +return $config; \ No newline at end of file diff --git a/lib/Rails/Console/ApplicationConsole.php b/lib/Rails/Console/ApplicationConsole.php new file mode 100755 index 0000000..6250cf5 --- /dev/null +++ b/lib/Rails/Console/ApplicationConsole.php @@ -0,0 +1,145 @@ +argv = !empty($_SERVER['argv']) ? $_SERVER['argv'] : []; + array_shift($this->argv); + $this->mainArgv = array_shift($this->argv); + } + + public function params() + { + return $this->params; + } + + public function run() + { + switch ($this->mainArgv) { + case 'generate': + $gen = new Generators\Generator($this); + $gen->parseCmd(); + break; + + case 'assets': + $rules = [ + 'assets' => '', + 'action' => '' + ]; + + $opts = new Zend\Console\Getopt($rules); + + $argv = $opts->getArguments(); + if (empty($argv[1])) { + $this->terminate("Missing argument 2"); + } + + \Rails::resetConfig('production'); + + switch ($argv[1]) { + case 'compile:all': + \Rails::assets()->setConsole($this); + \Rails::assets()->compileAll(); + break; + + case (strpos($argv[1], 'compile:') === 0): + $parts = explode(':', $argv[1]); + if (empty($parts[1])) { + $this->terminate("Missing asset name to compile"); + } + \Rails::assets()->setConsole($this); + \Rails::assets()->compileFile($parts[1]); + break; + + default: + $this->terminate("Unknown action for assets"); + break; + } + + break; + + case 'routes': + $routes = $this->createRoutes(); + + $rules = [ + 'routes' => '', + 'f-s' => '' + ]; + + $opts = new Zend\Console\Getopt($rules); + + if ($filename = $opts->getOption('f')) { + if (true === $filename) { + $logFile = \Rails::config()->paths->log->concat('routes.log'); + } else { + $logFile = \Rails::root() . '/' . $filename; + } + file_put_contents($logFile, $routes); + } + + $this->write($routes); + break; + } + } + + protected function createRoutes() + { + $router = \Rails::application()->dispatcher()->router(); + + $routes = [ + ['root', '', '/', $router->rootRoute()->to()] + ]; + + foreach ($router->routes() as $route) { + $routes[] = [ + $route->alias() ?: '', + strtoupper(implode(', ', $route->via())), + '/' . $route->url(), + $route->to() + ]; + } + + $aliasMaxLen = 0; + $viaMaxLen = 0; + $pathMaxLen = 0; + $toMaxLen = 0; + + foreach ($routes as $route) { + $aliasLen = strlen($route[0]); + $viaLen = strlen($route[1]); + $pathLen = strlen($route[2]); + + if ($aliasLen > $aliasMaxLen) + $aliasMaxLen = $aliasLen; + if ($viaLen > $viaMaxLen) + $viaMaxLen = $viaLen; + if ($pathLen > $pathMaxLen) + $pathMaxLen = $pathLen; + } + + $lines = []; + + foreach ($routes as $route) { + $route[0] = str_pad($route[0], $aliasMaxLen, ' ', STR_PAD_LEFT); + $route[1] = str_pad($route[1], $viaMaxLen, ' '); + $route[2] = str_pad($route[2], $pathMaxLen, ' '); + $lines[] = implode(' ', $route); + } + + return implode("\n", $lines); + } +} \ No newline at end of file diff --git a/lib/Rails/Console/Console.php b/lib/Rails/Console/Console.php new file mode 100755 index 0000000..1c2b4e0 --- /dev/null +++ b/lib/Rails/Console/Console.php @@ -0,0 +1,16 @@ +write($message); + exit; + } +} diff --git a/lib/Rails/Console/Generators/Generator.php b/lib/Rails/Console/Generators/Generator.php new file mode 100755 index 0000000..7a5cae3 --- /dev/null +++ b/lib/Rails/Console/Generators/Generator.php @@ -0,0 +1,99 @@ +console = $console; + + $rules = [ + 'generator' => '', + 'model' => '', + 'f|force' => '', + ]; + + $this->opts = new Zend\Console\Getopt($rules); + } + + public function parseCmd() + { + $argv = $this->opts->getArguments(); + + if (!$argv[1]) { + $console->terminate('Missing generator'); + } + + try { + switch ($argv[1]) { + case 'model': + $this->generateModel(); + break; + + case 'controller': + $this->generateController(); + break; + + case 'db-schema': + Toolbox\DbTools::generateSchemaFiles(); + break; + + default: + $this->terminate( + sprintf("Unknown generator for %s", $argv[1]) + ); + } + } catch (FileGenerator\Exception\ExceptionInterface $e) { + $this->terminate( + "Error: " . $e->getMessage() + ); + } + } + + protected function generateModel() + { + $rules = [ + 'name' => '', + ]; + + $opts = $this->opts->addRules($rules); + $argv = $opts->getArguments(); + + if (empty($argv[2])) { + $this->console->terminate("Missing name for model"); + } + + $name = $argv[2]; + $options = $opts->getOptions(); + + FileGenerators\ModelGenerator::generate($name, $options, $this->console); + } + + protected function generateController() + { + $rules = [ + 'name' => '', + ]; + + $opts = $this->opts->addRules($rules); + $argv = $opts->getArguments(); + + if (empty($argv[2])) { + $this->console->terminate("Missing name for controller"); + } + + $name = $argv[2]; + $options = $opts->getOptions(); + + FileGenerators\ControllerGenerator::generate($name, $options, $this->console); + } +} \ No newline at end of file diff --git a/lib/Rails/Console/Generators/Model.php b/lib/Rails/Console/Generators/Model.php new file mode 100755 index 0000000..6448dd0 --- /dev/null +++ b/lib/Rails/Console/Generators/Model.php @@ -0,0 +1,7 @@ +title ?: $this->_title; + + // if (!empty($params['scope'])) { + // $repl = '%scope%'; + // $title = str_replace($repl, $params['scope'], $title); + // } + + return $title; + } + + public function shift_trace() + { + if (!$this->_reduced_trace) + $this->_reduced_trace = $this->getTrace(); + array_shift($this->_reduced_trace); + } + + public function get_trace() + { + return $this->_reduced_trace; + } + + public function skip_info() + { + return $this->skip_info; + } + + public function status() + { + return $this->status; + } + + public function postMessage() + { + return ''; + } +} \ No newline at end of file diff --git a/lib/Rails/Exception/InvalidArgumentException.php b/lib/Rails/Exception/InvalidArgumentException.php new file mode 100755 index 0000000..1d6ef62 --- /dev/null +++ b/lib/Rails/Exception/InvalidArgumentException.php @@ -0,0 +1,7 @@ +error_data; + else + $this->error_data = $edata; + } +} \ No newline at end of file diff --git a/lib/Rails/Exception/PHPError/Catchable.php b/lib/Rails/Exception/PHPError/Catchable.php new file mode 100755 index 0000000..a93d933 --- /dev/null +++ b/lib/Rails/Exception/PHPError/Catchable.php @@ -0,0 +1,7 @@ +title()) { + if ($e instanceof PHPError) { + if ($errorScope) { + $error_name = $e->title() . ' in ' . $errorScope; + } else { + $error_name = $e->title(); + } + } else { + $error_name = $e->title(); + } + } else { + if ($errorScope) { + $error_name = get_class($e) . ' in ' . $errorScope; + } else { + $error_name = get_class($e) . ' thrown'; + } + } + + if (Rails::cli()) { + $error_name = '*** ' . $error_name . ' ***'; + } + + $html = ''; + $html .= '

'.$error_name.'

'."\n"; + + $html .= '
'.$e->getMessage();
+        if ($e instanceof RailsException && ($postMessage = $e->postMessage())) {
+            $html .= "\n" . $postMessage;
+        }
+        if (!empty($options['extraMessages'])) {
+            $html .= "\n" . $options['extraMessages'];
+        }
+        
+        if ($e instanceof RailsException && $e->skip_info()) {
+            $html .= "
\n"; + } else { + $base_dir = Rails::root() . DIRECTORY_SEPARATOR; + $rails_base_dir = Rails::path() . '/'; + + $file = $line = $context_args = null; + + $trace = $e->getTrace(); + + if ($e instanceof PHPError) { + if (!empty($e->error_data()['errargs'])) + $context_args = $e->error_data()['errargs']; + } else { + if (isset($trace[0]['args'])) + $context_args = $trace[0]['args']; + } + + $trace = $e->getTraceAsString(); + + $trace = str_replace([Rails::path(), Rails::root() . '/'], ['Rails', ''], $trace); + + $file = self::pretty_path($e->getFile()); + + $line = $e->getLine(); + + $html .= ''; + $html .= "\n"; + $html .= self::rootAndTrace(); + $html .= '
' . $trace . '
'; + + if ($context_args) { + ob_start(); + var_dump($context_args); + $context = ob_get_clean(); + } else { + $context = ''; + } + $html .= self::showContextLink(); + + if (!Rails::cli()) { + $html .= ''; + } + } + + return $html; + } + + /** + * Cleans up the HTML report created by create_html_report() for logging purposes. + */ + static public function cleanup_report($log) + { + return strip_tags(str_replace(self::removableLines(), ['', "\nError context:\n"], $log)); + } + + static public function pretty_path($path) + { + if (strpos($path, Rails::path()) === 0) + return 'Rails' . substr($path, strlen(Rails::path())); + elseif (strpos($path, Rails::root()) === 0) + return substr($path, strlen(Rails::root())); + else + return $path; + } + + /** + * Experimental (like everything else): + * These functions will be used to skip lines in the trace, + * just to give the title of the exception a more accurate + * scope (the function/method where the error occured). + * If the validation returns true, a line will be skipped. + * This is particulary useful when the exception was thrown by + * the error handler, or the like. + */ + static protected function traceSkippers() + { + return [ + function ($trace) { + if ( + isset($trace[0]['class']) && + isset($trace[0]['function']) && + $trace[0]['class'] == 'Rails' && + $trace[0]['function'] == 'errorHandler' + ) { + return true; + } + }, + + function ($trace) { + if ( + isset($trace[0]['class']) && + isset($trace[0]['function']) && + $trace[0]['class'] == 'Rails\ActiveRecord\Base' && + $trace[0]['function'] == '__get' + ) { + return true; + } + } + ]; + } + + static private function rootAndTrace() + { + $lines = 'Rails::root(): ' . Rails::root() . ""; + $lines .= '

Trace

'."\n"; + return $lines; + } + + static private function showContextLink() + { + $lines = 'Show error context'; + return $lines; + } + + static private function removableLines() + { + return [self::rootAndTrace(), self::showContextLink()]; + } + + static protected function findErrorScope($e) + { + $tr = $e->getTrace(); + + foreach (self::traceSkippers() as $skipper) { + if (true === $skipper($tr)) { + array_shift($tr); + } + } + + if (isset($tr[0]['class']) && isset($tr[0]['function']) && isset($tr[0]['type'])) { + if ($tr[0]['type'] == '->') { + // # looks better than -> + $type = '#'; + // $type = '->'; + } else { + $type = $tr[0]['type']; + } + $errorScope = $tr[0]['class'] . $type . $tr[0]['function'] . ''; + } elseif (!$e instanceof PHPError && isset($tr[0]['function'])) { + $errorScope = $tr[0]['function'] . ''; + } else { + $errorScope = ''; + } + + return $errorScope; + } +} \ No newline at end of file diff --git a/lib/Rails/Exception/RuntimeException.php b/lib/Rails/Exception/RuntimeException.php new file mode 100755 index 0000000..eb657d6 --- /dev/null +++ b/lib/Rails/Exception/RuntimeException.php @@ -0,0 +1,7 @@ +setLocale($locale); + else { + $this->_locale = $this->config()->default_locale; + } + $this->_load_rails_locale(); + $this->loadLocale(); + } + + public function config() + { + return Rails::application()->config()->i18n; + } + + public function locale($val = null) + { + if ($val !== null) { + throw new \Exception("Deprecated - use setLocale()"); + } else + return $this->_locale; + } + + public function setLocale($value) + { + if (!is_string($value)) { + throw new Exception\InvalidArgumentException( + sprintf('Locale value must be a string, %s passed', gettype($value)) + ); + } elseif ($this->_locale == $value) { + return; + } + $this->_locale = $value; + $this->_load_rails_locale($value); + $this->loadLocale($value); + } + + public function t($name, array $params = []) + { + if (is_array($name)) { + $params = $name; + $name = array_shift($params); + } elseif (!is_string($name)) { + throw new Exception\InvalidArgumentException( + sprintf('Argument must be either an array or string, %s passed', gettype($params)) + ); + } + + if (is_null($tr = $this->_get_translation($this->_locale, $name)) + && ($this->_locale != $this->config()->default_locale ? is_null($tr = $this->_get_translation($this->config()->default_locale, $name)) : true)) + { + return false; + } + + if (is_int(strpos($tr, '%{'))) { + foreach ($params as $k => $param) { + $tr = str_replace('%{'.$k.'}', $param, $tr); + unset($params[$k]); + } + } + if ($params) { + call_user_func_array('sprintf', array_merge(array($tr), $params)); + } + + return $tr; + } + + public function available_locales() + { + if (!is_array($this->_available_locales)) { + $this->_get_available_locales(); + } + return $this->_available_locales; + } + + public function defaultLocale() + { + return $this->config()->default_locale; + } + + private function _get_translation($lang, $name) + { + $tr = null; + + if (isset($this->_tr[$lang])) { + if (is_int(strpos($name, '.'))) { + $tr = $this->_tr[$lang]; + foreach (explode('.', $name) as $idx) { + if (isset($tr[$idx])) { + $tr = $tr[$idx]; + } else { + break; + } + } + + if (!is_string($tr)) + $tr = null; + } else { + if (isset($this->_tr[$lang][$name])) + $tr = $this->_tr[$lang][$name]; + } + } + + return $tr; + } + + private function _get_available_locales() + { + $dh = opendir($this->config()->path); + + $this->_available_locales = array(); + + while (!is_bool($file = readdir($dh))) { + if ($file == '.' || $file == '..') + continue; + $locale = pathinfo($file, PATHINFO_FILENAME); + $this->_available_locales[] = $locale; + } + closedir($dh); + } + + /** + * Loads locale file. + * If not a full path (i.e. doesn't start with / or x:), it'd be + * taken as relative path to locales path (i.e. config/locales). In this + * case, the extension of the file must be omitted. + * + * $i18n->loadLocale("/home/www/foo/bar/es.yml"); + * $i18n->loadLocale("es"); <- loads config/locales/es.php|yml + * $i18n->loadLocale("subdir/es"); <- loads config/locales/subdir/es.php|yml + * $i18n->loadLocale("/some/path/locales/%locale%.yml"); %locale% will be replaced with current locale + */ + public function loadLocale($locale = null) + { + !$locale && $locale = $this->_locale; + + if (in_array($locale, $this->loaded_locales)) { + return; + } + + if (is_int(($pos = strpos($locale, '%locale%')))) { + $locale = substr_replace($locale, $this->locale(), $pos, 8); + } + + if (substr($locale, 0, 1) == '/' || substr($locale, 1, 1) == ':') { + $file = $locale; + } else { + $patt = $this->config()->path . '/' . $locale . '.{php,yml}'; + $files = glob($patt, GLOB_BRACE); + if ($files) { + $file = $files[0]; + } else { + return false; + } + } + + $ext = pathinfo($file, PATHINFO_EXTENSION); + if ($ext == 'yml') { + $locale_data = Rails\Yaml\Parser::readFile($file); + } else { + $locale_data = require $file; + } + $this->_tr = array_merge_recursive($this->_tr, $locale_data); + + $this->loaded_locales[] = $locale; + return true; + } + + private function _load_rails_locale($locale = null) + { + !func_num_args() && $locale = $this->_locale; + + + $locale = ucfirst(strtolower($locale)); + $class_name = 'Rails\I18n\Locales\\' . $locale; + + if (class_exists($class_name, false)) + return; + + $file = __DIR__ . '/Locales/' . $locale . '.'; + $exts = ['php']; + + foreach ($exts as $ext) { + $f = $file.$ext; + if (is_file($f)) { + require $f; + $obj = new $class_name(); + $this->_tr = array_merge_recursive($this->_tr, $obj->tr()); + break; + } + } + + /** + * If there's nothing in the array, it means we've just tried to load the default + * application locale for Rails that isn't supported. Load english locale instead. + */ + if (!$this->_tr) + $this->_load_rails_locale('en'); + } +} \ No newline at end of file diff --git a/lib/Rails/I18n/Locales/AbstractLocales.php b/lib/Rails/I18n/Locales/AbstractLocales.php new file mode 100755 index 0000000..15765c2 --- /dev/null +++ b/lib/Rails/I18n/Locales/AbstractLocales.php @@ -0,0 +1,12 @@ +translations; + } +} diff --git a/lib/Rails/I18n/Locales/En.php b/lib/Rails/I18n/Locales/En.php new file mode 100755 index 0000000..60fd23b --- /dev/null +++ b/lib/Rails/I18n/Locales/En.php @@ -0,0 +1,54 @@ + [ + 'actionview' => [ + 'helper' => [ + 'date' => [ + 'less_than_a_minute'=> 'Less than a minute', + 'one_minute' => '1 minute', + 'x_minutes' => '%{t} minutes', + 'about_one_hour' => 'about one hour', + 'about_x_hours' => 'about %{t} hours', + 'one_day' => '1 day', + 'x_days' => '%{t} days', + 'about_one_month' => 'about 1 month', + 'x_months' => '%{t} months', + 'about_one_year' => 'about 1 year', + 'over_a_year' => 'over a year', + 'almost_two_years' => 'almost 2 years', + 'about_x_years' => 'about %{t} years' + ] + ] + ], + + 'errors' => [ + 'messages' => [ + "inclusion" => "is not included in the list", + "exclusion" => "is reserved", + "invalid" => "is invalid", + "confirmation" => "doesn't match confirmation", + "accepted" => "must be accepted", + "empty" => "can't be empty", + "blank" => "can't be blank", + "too_long" => "is too long (maximum is %{count} characters)", + "too_short" => "is too short (minimum is %{count} characters)", + "wrong_length" => "is the wrong length (should be %{count} characters)", + "not_a_number" => "is not a number", + "not_an_integer" => "must be an integer", + "greater_than" => "must be greater than %{count}", + "greater_than_or_equal_to" => "must be greater than or equal to %{count}", + "equal_to" => "must be equal to %{count}", + "less_than" => "must be less than %{count}", + "less_than_or_equal_to" => "must be less than or equal to %{count}", + "odd" => "must be odd", + "even" => "must be even", + + # These belong to activerecord + "uniqueness" => "must be unique" + ] + ] + ]]; +} \ No newline at end of file diff --git a/lib/Rails/I18n/Locales/Es.php b/lib/Rails/I18n/Locales/Es.php new file mode 100755 index 0000000..3cbbd37 --- /dev/null +++ b/lib/Rails/I18n/Locales/Es.php @@ -0,0 +1,54 @@ + [ + 'actionview' => [ + 'helper' => [ + 'date' => [ + 'less_than_a_minute'=> 'menos de un minuto', + 'one_minute' => '1 minuto', + 'x_minutes' => '%{t} minutos', + 'about_one_hour' => 'alrededor de una hora', + 'about_x_hours' => 'alrededor de %{t} horas', + 'one_day' => '1 día', + 'x_days' => '%{t} días', + 'about_one_month' => 'alrededor de 1 mes', + 'x_months' => '%{t} meses', + 'about_one_year' => 'alrededor 1 año', + 'over_a_year' => 'más de un año', + 'almost_two_years' => 'casi 2 años', + 'about_x_years' => 'alrededor de %{t} años' + ] + ] + ], + + 'errors' => [ + 'messages' => [ + "inclusion" => "is not included in the list", + "exclusion" => "is reserved", + "invalid" => "is invalid", + "confirmation" => "doesn't match confirmation", + "accepted" => "must be accepted", + "empty" => "can't be empty", + "blank" => "can't be blank", + "too_long" => "is too long (maximum is %{count} characters)", + "too_short" => "is too short (minimum is %{count} characters)", + "wrong_length" => "is the wrong length (should be %{count} characters)", + "not_a_number" => "is not a number", + "not_an_integer" => "must be an integer", + "greater_than" => "must be greater than %{count}", + "greater_than_or_equal_to" => "must be greater than or equal to %{count}", + "equal_to" => "must be equal to %{count}", + "less_than" => "must be less than %{count}", + "less_than_or_equal_to" => "must be less than or equal to %{count}", + "odd" => "must be odd", + "even" => "must be even", + + # These belong to activerecord + "uniqueness" => "must be unique" + ] + ] + ]]; +} \ No newline at end of file diff --git a/lib/Rails/Loader/Exception/ClassNotFoundException.php b/lib/Rails/Loader/Exception/ClassNotFoundException.php new file mode 100755 index 0000000..af1dae2 --- /dev/null +++ b/lib/Rails/Loader/Exception/ClassNotFoundException.php @@ -0,0 +1,6 @@ +addPaths($paths); + } + + $this->loadRequiredClasses(); + } + + public function setComposerAutoload(\Composer\Autoload\ClassLoader $loader) + { + $this->composerAutoloader = $loader; + } + + public function addPath($path) + { + $this->addPaths((array)$path); + } + + public function addPaths(array $paths) + { + $this->paths = array_merge($this->paths, $paths); + } + + public function loadClass($class_name) + { + if ($this->runAutoloaders($class_name)) { + return; + } + + if ($this->composerAutoloader->loadClass($class_name)) { + return; + } + + if (is_int(strpos($class_name, '\\'))) { + $parts = explode('\\', $class_name); + $file = array_pop($parts); + $class_file_path = implode('/', $parts) . '/' . $file . '.php'; + } else + $class_file_path = str_replace('_', '/', $class_name) . '.php'; + + $paths = $this->paths; + $found = false; + + foreach ($paths as $path) { + if ($found = is_file(($class_file = $path . '/' . $class_file_path))) { + break; + } + } + + if (!$found) { + require_once __DIR__ . '/Exception/FileNotFoundException.php'; + throw new Exception\FileNotFoundException(sprintf("Couldn't find file for class %s, searched for %s in:\n%s", $class_name, $class_file_path, implode("\n", $paths))); + } + + require $class_file; + + if (!class_exists($class_name, false) && !interface_exists($class_name, false) && !trait_exists($class_name, false)) { + require_once __DIR__ . '/Exception/ClassNotFoundException.php'; + throw new Exception\ClassNotFoundException(sprintf("File %s doesn't contain class/interface/trait %s.", $class_file, $class_name)); + } + } + + /** + * Autoloaders must return true is the class was + * successfuly loaded. + */ + public function registerAutoloader(callable $function) + { + $this->class_autoloaders[] = $function; + } + + public function runAutoloaders($class_name) + { + foreach ($this->class_autoloaders as $autoload) { + if (is_array($autoload)) { + $object = $autoload[0]; + $method = $autoload[1]; + + if (true === $object->$method($class_name)) { + return true; + } + } elseif (is_string($autoload)) { + if (true === call_user_func($autoload, $class_name)) + return true; + } elseif ($autoload instanceof Closure) { + if (true === $autoload($class_name)) + return true; + } else { + require_once __DIR__ . '/Exception/RuntimeException.php'; + throw new Exception\RuntimeException(sprintf("Invalid autoloader type (%s)", gettype($autoload))); + } + } + + return false; + } + + /** + * Loader uses ActiveRecord and ActionMailer. These classes must be available. + */ + protected function loadRequiredClasses() + { + } +} \ No newline at end of file diff --git a/lib/Rails/Log/Formatter/Simple.php b/lib/Rails/Log/Formatter/Simple.php new file mode 100755 index 0000000..b9206d8 --- /dev/null +++ b/lib/Rails/Log/Formatter/Simple.php @@ -0,0 +1,43 @@ +originalFormat = $this->format; + $this->format = trim(str_replace('%priorityName%', '', $this->format), ': '); + } + + /** + * Hack: write log with date. + */ + if (!empty($event['extra']['date'])) { + if (!$this->originalFormat) { + $this->originalFormat = $this->format; + } + $this->format = '[%timestamp%] ' . $this->format; + unset($event['extra']['date']); + } + + $ret = parent::format($event); + + if ($this->originalFormat) { + $this->format = $this->originalFormat; + $this->originalFormat = null; + } + + return $ret; + } +} \ No newline at end of file diff --git a/lib/Rails/Log/Logger.php b/lib/Rails/Log/Logger.php new file mode 100755 index 0000000..3baaadc --- /dev/null +++ b/lib/Rails/Log/Logger.php @@ -0,0 +1,116 @@ + 'EMERGENCY', + self::ALERT => 'ALERT', + self::CRIT => 'CRITICAL', + self::ERR => 'ERROR', + self::WARN => 'WARNING', + self::NOTICE => 'NOTICE', + self::INFO => 'INFO', + self::DEBUG => 'DEBUG', + self::NONE => 'NONE', + ); + + protected $name = ''; + + public function __construct(array $options = []) + { + if (isset($options['name'])) { + $this->name = $options['name']; + unset($optiona['name']); + } + parent::__construct(); + } + + public function emergency($message, $extra = []) + { + return $this->emerg($message, $extra); + } + + public function critical($message, $extra = []) + { + return $this->crit($message, $extra); + } + + public function error($message, $extra = []) + { + return $this->err($message, $extra); + } + + public function warning($message, $extra = []) + { + return $this->warn($message, $extra); + } + + public function none($message) + { + return $this->log(self::NONE, $message); + } + + public function vars(array $vars) + { + ob_start(); + foreach ($vars as $var) { + var_dump($var); + } + $message = ob_get_clean(); + $message .= "\n"; + return $this->none($message); + } + + /** + * Additional text can be passed through $options['extraMessages']. + */ + public function exception(\Exception $e, array $options = []) + { + $message = \Rails\Exception\Reporting\Reporter::create_report($e, $options); + $message = \Rails\Exception\Reporting\Reporter::cleanup_report($message); + + return $this->message($message); + } + + /** + * Adds date-time and request data, if any. + */ + public function message($err) + { + return $this->none($this->buildErrorMessage($err)); + } + + private function buildErrorMessage($err, $requestInfo = true) + { + if ($requestInfo) { + $request = ' ' . $this->buildRequestInfo(); + } else { + $request = ''; + } + + $message = date('[d-M-Y H:i:s T]') . $request . "\n"; + $message .= $err; + $message = trim($message); + $message .= "\n"; + return $message; + } + + private function buildRequestInfo() + { + if (\Rails::application()->dispatcher() && \Rails::application()->dispatcher()->request()) { + $method = \Rails::application()->dispatcher()->request()->method(); + $route = \Rails::application()->dispatcher()->request()->fullPath(); + $request = '[' . $method . '] ' . $route; + } elseif (\Rails::cli()) { + $request = '[cli]'; + } else { + $request = ''; + } + return $request; + } +} diff --git a/lib/Rails/Panel/assets/panel.css b/lib/Rails/Panel/assets/panel.css new file mode 100755 index 0000000..8350359 --- /dev/null +++ b/lib/Rails/Panel/assets/panel.css @@ -0,0 +1,6 @@ +/** + *= require bootstrap + */ +body { + padding-top: 60px; +} \ No newline at end of file diff --git a/lib/Rails/Panel/controllers/AdminController.php b/lib/Rails/Panel/controllers/AdminController.php new file mode 100755 index 0000000..5f1283d --- /dev/null +++ b/lib/Rails/Panel/controllers/AdminController.php @@ -0,0 +1,140 @@ +parse(); + + $this->response()->headers()->setContentType('text/css'); + $this->render(['text' => $parser->parsedFile()]); + } + + final public function genTableData() + { + Toolbox\DbTools::generateSchemaFiles(); + $this->toIndex('Database schema files updated'); + } + + public function createFiles() + { + if ($this->request()->isPost()) { + Rails::resetConfig('development'); + + $base_name = trim($this->params()->file_name); + if ($base_name) { + if ($this->params()->type['controller']) + $this->createControllerFile($base_name); + if ($this->params()->type['model']) + $this->createModelFile($base_name); + if ($this->params()->type['helper']) + $this->createHelperFile($base_name); + } + + Rails::application()->setPanelConfig(); + } + } + + public function showRoutes() + { + $router = Rails::application()->dispatcher()->router(); + $this->routes = [ + ['root', null, '/', $router->rootRoute()->to()], + ['rails_panel', null, '/' . $router->panelRoute()->url(), ''], + ]; + + foreach ($router->routes() as $route) { + if ($route instanceof Route\HiddenRoute) { + continue; + } + $this->routes[] = [ + $route->alias(), strtoupper(implode(', ', $route->via())), '/' . $route->url(), $route->to() + ]; + } + } + + public function compileAssets() + { + $this->error = ''; + if ($this->request()->isPost()) { + Rails::resetConfig('production'); + + try { + if ($this->params()->all) { + Rails::assets()->compileAll(); + } elseif ($this->params()->file) { + $file = $this->params()->file; + Rails::assets()->compileFile($file); + } + } catch (Rails\Assets\Parser\Javascript\ClosureApi\Exception\ErrorsOnCodeException $e) { + if ($e instanceof Rails\Assets\Parser\Javascript\ClosureApi\Exception\ErrorsOnCodeException) { + Rails::log()->error( + sprintf( + "[%s] Asset compilation error for file %s\n%s", + date('Y-m-d H:i:s'), + $file_path . '.' . $ext, + $e->getMessage() + ) + ); + $message = sprintf("ClosureAPI reported an error - JS file was saved to %s for verfications, error was logged.
%s
", + Rails\Assets\Parser\Javascript\ClosureApi\ClosureApi::errorFile(), $e->getMessage()); + } else { + // throw $e; + // $message = sprintf('%s raised: %s', get_class($e), $e->getMessage()); + } + + $this->error = $message; + } + + Rails::resetConfig('development'); + Rails::application()->setPanelConfig(); + } + } + + final protected function toIndex($notice) + { + $url = '/' . Rails::application()->config()->rails_panel_path; + parent::redirectTo(array($url, 'notice' => $notice)); + } + + public function redirectTo($redirect_params, array $params = array()) + { + $redirect_params[0] = '/' . Rails::application()->config()->rails_panel_path . '/' . $redirect_params[0]; + parent::redirectTo($redirect_params, $params); + } + + private function createControllerFile($name, $options = []) + { + Toolbox\FileGenerators\ControllerGenerator::generate($name, $options); + } + + private function createModelFile($name, $options = []) + { + Toolbox\FileGenerators\ModelGenerator::generate($name, $options); + } + + private function createHelperFile($base_name) + { + $name = $base_name . '_helper.php'; + $path = Rails::config()->paths->helpers; + + if (is_file($path . '/' . $name)) + return; + + $class_name = Rails::services()->get('inflector')->camelize($base_name) . 'Helper'; + $contents = " [ + 'isUserAllowed' + ] + ]; + } + + final protected function isUserAllowed() + { + if (!Rails::application()->validateSafeIps()) { + $this->render(['action' => 'forbidden'], ['status' => 403]); + } + } +} \ No newline at end of file diff --git a/lib/Rails/Panel/helpers/ApplicationHelper.php b/lib/Rails/Panel/helpers/ApplicationHelper.php new file mode 100755 index 0000000..6245f41 --- /dev/null +++ b/lib/Rails/Panel/helpers/ApplicationHelper.php @@ -0,0 +1,14 @@ +base()->linkTo($link, $url_params, $attrs); + } else { + $base_path = Rails::application()->router()->basePath(); + $attrs['href'] = $base_path . '/' . Rails::application()->config()->rails_panel_path . '/' . substr($url_params, 1); + return $this->contentTag('a', $link, $attrs); + } + } +} \ No newline at end of file diff --git a/lib/Rails/Panel/traits/AdminController.php b/lib/Rails/Panel/traits/AdminController.php new file mode 100755 index 0000000..89dfa8c --- /dev/null +++ b/lib/Rails/Panel/traits/AdminController.php @@ -0,0 +1,6 @@ + +

Compile assets

+ +
+ error) : ?> +
error ?>
+ + + formTag([], ['style' => 'margin-bottom:0px'], function(){ ?> + + + +
+
Or
+
+
+ formTag([], ['style' => 'margin-bottom:0px;'], function(){ ?> + + + + + + + + + + + + + +
Assets folderFile name
config()->assets->prefix . '/' ?>textFieldTag('file', $this->params()->file, ['size' => 40, 'autofocus', 'placeholder' => 'E.g. application.css']) ?>
submitTag('Compile file', ['class' => 'btn btn-primary']) ?>
+
+ diff --git a/lib/Rails/Panel/views/admin/create_files.php b/lib/Rails/Panel/views/admin/create_files.php new file mode 100755 index 0000000..0af096a --- /dev/null +++ b/lib/Rails/Panel/views/admin/create_files.php @@ -0,0 +1,33 @@ +
+

Create files

+formTag(null, ['class' => 'form-horizontal well'], function() { ?> +
+ +
+ +
+
+
+
+
+ + + +
+ +
+ + + +
+ +
+ + + +
+
+
+ +
+ \ No newline at end of file diff --git a/lib/Rails/Panel/views/admin/forbidden.php b/lib/Rails/Panel/views/admin/forbidden.php new file mode 100755 index 0000000..3efd875 --- /dev/null +++ b/lib/Rails/Panel/views/admin/forbidden.php @@ -0,0 +1 @@ +

FORBIDDEN FOR CLIENT request()->remoteIp() ?>

\ No newline at end of file diff --git a/lib/Rails/Panel/views/admin/index.php b/lib/Rails/Panel/views/admin/index.php new file mode 100755 index 0000000..e33717c --- /dev/null +++ b/lib/Rails/Panel/views/admin/index.php @@ -0,0 +1 @@ +

Use the menu above.

diff --git a/lib/Rails/Panel/views/admin/show_routes.php b/lib/Rails/Panel/views/admin/show_routes.php new file mode 100755 index 0000000..9debb90 --- /dev/null +++ b/lib/Rails/Panel/views/admin/show_routes.php @@ -0,0 +1,20 @@ +

Routes

+ + + + + + + + + + routes as $route) : ?> + + + + + + + + +
PrefixVerbURI PatternController#Action
diff --git a/lib/Rails/Panel/views/layouts/application.php b/lib/Rails/Panel/views/layouts/application.php new file mode 100755 index 0000000..0d85ea1 --- /dev/null +++ b/lib/Rails/Panel/views/layouts/application.php @@ -0,0 +1,43 @@ + + + + <?= Rails::application()->name() ?> + + + + +
+ + +
+
+ +
+
+ content() ?> +
+
+

© 2013 PHP on Rails

+
+ + + diff --git a/lib/Rails/Paths/Path.php b/lib/Rails/Paths/Path.php new file mode 100755 index 0000000..2484eeb --- /dev/null +++ b/lib/Rails/Paths/Path.php @@ -0,0 +1,83 @@ +basePaths = $basePaths; + $this->path = $path; + } + + public function __toString() + { + return $this->fullPath(); + } + + public function toString() + { + return $this->fullPath(); + } + + public function addBasePath($basePath) + { + $this->basePaths[] = $basePath; + } + + public function basePaths(array $basePaths = null) + { + if (null === $basePaths) { + return $this->baePaths; + } else { + $this->basePaths = $basePaths; + } + } + + public function path($path = null) + { + if (null === $path) { + return $this->path; + } else { + $this->path = $path; + } + } + + public function setPath($path) + { + $this->path = $path; + return $this; + } + + public function setBasePaths(array $basePaths) + { + $this->basePaths = $basePaths; + return $this; + } + + public function fullPath() + { + $basePath = implode('/', $this->basePaths); + if ($basePath) { + $basePath .= '/'; + } + return $basePath . $this->path; + } + + /** + * Concats fullPath with additional sub paths (to point to a file, + * for example). This is therefore intended to be used if this Path + * points to a folder. + * + * @param string|array $paths paths to concat to the fullPath + */ + public function concat($paths) + { + if (is_array($paths)) + $paths = implode('/', $paths); + return $this->fullPath() . '/' . $paths; + } +} \ No newline at end of file diff --git a/lib/Rails/Paths/Paths.php b/lib/Rails/Paths/Paths.php new file mode 100755 index 0000000..91abc64 --- /dev/null +++ b/lib/Rails/Paths/Paths.php @@ -0,0 +1,15 @@ +offsetExists($prop)) { + return $this->offsetGet($prop); + } + throw new \Rails\Exception\RuntimeException( + sprintf("Trying to get undefined path %s", $prop) + ); + } +} \ No newline at end of file diff --git a/lib/Rails/Rails.php b/lib/Rails/Rails.php new file mode 100755 index 0000000..821156b --- /dev/null +++ b/lib/Rails/Rails.php @@ -0,0 +1,520 @@ +resetConfig($environment); + } + + static public function loader() + { + return self::$loader; + } + + static public function assets() + { + if (!self::$assets) { + self::$assets = Rails\Assets\Assets::instance(); + + $prefix = str_replace('\\', '/', self::$config->assets->prefix); + + $router = self::application()->router(); + if ($router && $router->route() && $router->route()->isPanelRoute()) { + $basePaths = [ + realpath(Rails::path() . '/../../vendor/assets') + ]; + } else { + $basePaths = [ + str_replace('\\', '/', Rails::config()->paths->application) . $prefix, + str_replace('\\', '/', Rails::root() . '/lib') . $prefix, + str_replace('\\', '/', Rails::root() . '/vendor') . $prefix, + ]; + } + + $paths = []; + + foreach ($basePaths as $basePath) { + if (!is_dir($basePath)) { + continue; + } + + $dir = new DirectoryIterator($basePath); + + foreach ($dir as $file) { + if ($file->isDot() || !$file->isDir()) { + continue; + } + + $paths[] = $file->getPathname(); + } + } + + $customPaths = Rails::application()->config()->assets->paths->toArray(); + + $paths = array_merge($paths, $customPaths); + + self::$assets->addPaths($paths); + + self::$assets->addFilePatterns($this->config()->patterns->toArray(); + } + return self::$assets; + } + + static public function cache() + { + if (!self::$cache) + self::$cache = new Rails\Cache\Cache(self::application()->config()->cache_store); + return self::$cache; + } + + /** + * Returns logger set in config. + * If arguments are passed, they will be logged with vars(). + */ + static public function log() + { + if (!self::$config->logger) { + self::$config->logger = self::defaultLogger(); + } elseif (self::$config->logger instanceof \Closure) { + $closure = self::$config->logger; + self::$config->logger = $closure(); + } + + if (func_num_args()) { + return self::$config->logger->vars(func_get_args()); + } + + return self::$config->logger; + } + + static public function env() + { + return self::$env; + } + + static public function root() + { + return self::$root; + } + + static public function publicPath() + { + return self::$publicPath; + } + + static public function path() + { + return self::$path; + } + + static public function cli() + { + return self::$cli; + } + + static public function console() + { + if (!self::$console) { + self::$console = new Rails\Console\ApplicationConsole(); + } + return self::$console; + } + + static public function errorHandler($errno, $errstr, $errfile, $errline, $errargs) + { + $errtype = ''; + + switch ($errno) { + case E_WARNING: + $class_name = 'Warning'; + break; + + case E_NOTICE: + $class_name = 'Notice'; + break; + + case E_RECOVERABLE_ERROR: + $class_name = 'Catchable'; + break; + + case E_USER_NOTICE: + $class_name = 'UserNotice'; + break; + + case E_USER_WARNING: + $class_name = 'UserWarning'; + break; + + default: + /** + * When an "Unknown" error is triggered, the Loader isn't invoked for some reason. + * Hence the manual requires. + */ + require_once __DIR__ . '/Exception/ExceptionInterface.php'; + require_once __DIR__ . '/Exception/ExceptionTrait.php'; + require_once __DIR__ . '/Exception/ErrorException.php'; + require_once __DIR__ . '/Exception/PHPError/ExceptionInterface.php'; + require_once __DIR__ . '/Exception/PHPError/Base.php'; + require_once __DIR__ . '/Exception/PHPError/Unknown.php'; + $class_name = 'Unknown'; + $errtype = '[ErrNo '.$errno.']'; + break; + } + + $class = 'Rails\Exception\PHPError\\' . $class_name; + + if (strpos($errfile, self::$path) === 0) + $errfile = 'Rails' . substr($errfile, strlen(self::$path)); + + $extra_params = ['php_error' => true, 'errno' => $errno, 'errstr' => $errstr, 'errfile' => $errfile, 'errline' => $errline, 'errargs' => $errargs]; + + if ($errtype) + $errtype .= ': '; + + $e = new $class( + sprintf('%s%s ', $errtype, $errstr) + ); + + $e->error_data($extra_params); + + throw $e; + } + + static public function exceptionHandler($e) + { + if (self::$cli) { + $report = Rails\Exception\Reporting\Reporter::create_report($e); + $report = Rails\Exception\Reporting\Reporter::cleanup_report($report); + self::console()->terminate($report); + } else { + self::reportException($e); + } + } + + static public function systemExit() + { + if (!self::$systemExit) { + self::$systemExit = new Rails\SystemExit\SystemExit(); + } + return self::$systemExit; + } + + static public function runSystemExit() + { + if (self::$systemExit) { + self::$systemExit->run(); + } + } + + static public function services() + { + if (!self::$serviceManager) { + self::$serviceManager = new Rails\ServiceManager\ServiceManager(); + } + return self::$serviceManager; + } + + static private function reportException(Exception $e) + { + Rails\ActionView\ActionView::clean_buffers(); + + $parmas = []; + $params['status'] = $e instanceof Rails\Exception\ExceptionInterface ? $e->status() : 500; + + $params['report'] = Rails\Exception\Reporting\Reporter::create_report($e, $params); + + self::log()->message(Rails\Exception\Reporting\Reporter::cleanup_report($params['report'])); + + if (!self::application() || !self::application()->dispatcher()) { + try { + if (!self::application()) { + self::_early_exception($e, $params['report']); + } + self::application()->setDispatcher(); + } catch (Exception $e) { + self::_early_exception($e, $params['report']); + } + } + self::application()->dispatcher()->response()->headers()->status($params['status']); + + if (self::config()->action_controller->exception_handler && !self::config()->consider_all_requests_local) { + try { + self::$customExceptionHandlerRan = true; + $handler = Rails\ActionController\ActionController::load_exception_handler(self::$config->action_controller->exception_handler, $params['status']); + Rails::application()->set_controller($handler); + $handler->handleException($e); + } catch (Exception $e) { + self::_early_exception($e, $params['report']); + } + } else { + $renderer = new Rails\ActionController\Response\Error($e, $params); + self::application()->dispatcher()->response()->headers()->contentType('html'); + self::application()->dispatcher()->response()->body($renderer->render_view()->get_contents()); + } + self::application()->dispatcher()->respond(); + exit; + } + + static private function _early_exception($e, $html) + { + self::log()->exception($e); + + if (Rails::application() && Rails::application()->config()->consider_all_requests_local) { + echo $html; + } else { + echo "An error occured."; + } + exit; + } + + static private function setRailsConfig() + { + $paths = array_merge([ + get_include_path(), + self::$root . '/lib', + self::$root . '/vendor', + ]); + set_include_path(implode(PATH_SEPARATOR, $paths)); + + # Set loader. + require self::$path . '/Loader/Loader.php'; + + # Guessing ZF2 and Symfony paths + // if (self::$zf2Path) { + // $zf2Path = self::$zf2Path; + // } else { + // $zf2Path = realpath(self::$path . '/../../../ZF2/library'); + // if (!$zf2Path) { + // throw new Rails\Exception\RuntimeException( + // sprintf( + // "Can't find path to ZF2 library (tried: %s)", + // $zf2Path + // ) + // ); + // } + // } + + // if (self::$sfPath) { + // $sfPath = self::$sfPath; + // } else { + // $sfPath = realpath(self::$path . '/../../..'); + // if (!$sfPath) { + // throw new Rails\Exception\RuntimeException( + // sprintf( + // "Can't find path to Symfony (tried: %s)", + // $sfPath + // ) + // ); + // } + // } + + self::$loader = new Rails\Loader\Loader([ + dirname(self::$path), + self::$config->paths->models->toString(), + self::$config->paths->helpers, + self::$config->paths->controllers->toString(), + self::$config->paths->mailers->toString(), + self::$root . '/lib', + self::$root . '/vendor', + // $zf2Path, + // $sfPath, + ]); + + $autoloadFile = defined("COMPOSER_AUTOLOAD_FILE") ? + COMPOSER_AUTOLOAD_FILE : + Rails::path() . '/../../../autoload.php'; + self::$loader->setComposerAutoload(require $autoloadFile); + + spl_autoload_register([Rails::loader(), 'loadClass']); + + set_exception_handler('Rails::exceptionHandler'); + + register_shutdown_function('Rails::runSystemExit'); + + # This will be re-set in Rails_Application, setting also the customized second parameter. + set_error_handler('Rails::errorHandler', E_ALL); + } + + static private function defaultConfig() + { + return require __DIR__ . '/Config/default_config.php'; + } + + static private function defaultLogger() + { + $logLevel = (string)self::$config->log_level; + + switch ($logLevel) { + case '0': + case 'emergency': + $level = Zend\Log\Logger::EMERG; + break; + + case '1': + case 'alert': + $level = Zend\Log\Logger::ALERT; + break; + + case '2': + case 'critical': + $level = Zend\Log\Logger::CRIT; + break; + + case '3': + case 'error': + $level = Zend\Log\Logger::ERR; + break; + + case '4': + case 'warning': + $level = Zend\Log\Logger::WARN; + break; + + case '4': + case 'notice': + $level = Zend\Log\Logger::NOTICE; + break; + + case '5': + case 'info': + $level = Zend\Log\Logger::INFO; + break; + + case '6': + case 'debug': + $level = Zend\Log\Logger::DEBUG; + break; + + default: + $level = self::$config->log_level; + break; + } + + if (false !== $level && null !== $level) { + $filter = new Zend\Log\Filter\Priority($level); + $writer->addFilter($filter); + } + + $formatter = new Rails\Log\Formatter\Simple(); + + $writer = new Zend\Log\Writer\Stream(self::$config->log_file); + $writer->setFormatter($formatter); + + $logger = new Rails\Log\Logger(); + $logger->addWriter($writer); + + return $logger; + } +} diff --git a/lib/Rails/Routing/Exception/BadMethodCallException.php b/lib/Rails/Routing/Exception/BadMethodCallException.php new file mode 100755 index 0000000..767bae2 --- /dev/null +++ b/lib/Rails/Routing/Exception/BadMethodCallException.php @@ -0,0 +1,6 @@ +routeSet = $routeSet; + } + + public function __call($method, $params) + { + if ($method == 'namespace') { + call_user_func_array([$this, 'namespaced'], $params); + } + } + + /** + * Must specify a verb through the via parameter. + */ + public function match($url, $to = null, array $params = array()) + { + if (!$params && is_array($to)) { + $params = $to; + $to = null; + } + $this->createAndAddRoute($url, $to, $params); + } + + public function get($url, $to = null, array $params = array()) + { + $this->createRouteWithVerb('get', $url, $to, $params); + } + + public function post($url, $to = null, array $params = array()) + { + $this->createRouteWithVerb('post', $url, $to, $params); + } + + public function put($url, $to = null, array $params = array()) + { + $this->createRouteWithVerb('put', $url, $to, $params); + } + + public function patch($url, $to = null, array $params = array()) + { + $this->createRouteWithVerb('patch', $url, $to, $params); + } + + public function delete($url, $to = null, array $params = array()) + { + $this->createRouteWithVerb('delete', $url, $to, $params); + } + + public function root($to) + { + if (!$this->paths) { + $this->routeSet->set_root_route( + $this->createRoute( + self::ROOT_URL, + $to, + [ + 'via' => ['get'], + 'as' => 'root' + ] + ) + ); + } else { + $this->createAndAddRoute('', $to, ['via' => ['get'], 'as' => implode('_', $this->paths) . '_root']); + } + } + + public function scope($params, Closure $routesBlock) + { + if (is_string($params)) { + $params = [ 'path' => $params ]; + } elseif (!is_array($params)) { + $params = [ $params ]; + } + + if (isset($params['path'])) { + $this->paths[] = $params['path']; + unset($params['path']); + } + + $this->scopeParams[] = $params; + $routesBlock(); + array_pop($this->scopeParams); + array_pop($this->paths); + } + + public function member(Closure $block) + { + $this->scopeParams[] = ['on' => 'member']; + $block(); + array_pop($this->scopeParams); + } + + /** + * Namespace is a mix of module + path options. + */ + public function namespaced($namespace, Closure $block) + { + $this->paths[] = $namespace; + $this->modules[] = $namespace; + $this->routeNames[] = $namespace; + $block(); + array_pop($this->paths); + array_pop($this->modules); + array_pop($this->routeNames); + } + + /** + * @param string $name String with format /^\w+$/ + */ + public function resources($name, $params = null, Closure $block = null) + { + $this->resources[] = $name; + $this->resourceNesting[] = 'resources'; + $this->createRoutesWithResource(true, $params, $block); + array_pop($this->resources); + array_pop($this->resourceNesting); + } + + /** + * @param string $name String with format /^\w+$/ + */ + public function resource($name, $params = null, Closure $block = null) + { + $this->resource[] = $name; + $this->resourceNesting[] = 'resource'; + $this->createRoutesWithResource(false, $params, $block); + array_pop($this->resource); + array_pop($this->resourceNesting); + } + + public function concern($name, Closure $block) + { + $this->concernList[$name] = $block; + } + + private function createRoutesWithResource($multiple, $params, Closure $block = null) + { + if ($params instanceof Closure) { + $block = $params; + $params = []; + } elseif (!is_array($params)) { + $params = []; + } + + if ($block) { + $block(); + } + + if (isset($params['concerns'])) { + $params['concerns'] = (array)$params['concerns']; + foreach ($params['concerns'] as $name) { + if (!isset($this->concernList[$name])) { + throw new Exception\RuntimeException( + sprintf("No concern named %s was found") + ); + } + $this->concernList[$name](); + } + unset($params['concerns']); + } + + if (!isset($params['defaults']) || $params['defaults']) { + $actions = [ 'index', 'create', 'new', 'edit', 'show', 'update', 'destroy' ]; + + if (!$multiple) { + array_shift($actions); + } + + $only = !empty($params['only']) ? $params['only'] : []; + $except = !empty($params['except']) ? $params['except'] : []; + + unset($params['only'], $params['except']); + + foreach ($actions as $action) { + if ($only) { + $create = in_array($action, $only); + } elseif ($except) { + $create = !in_array($action, $except); + } else { + $create = true; + } + + if ($create) { + $this->addRoute($this->createResourceRoute($action, $params, $multiple)); + } + } + } + } + + private function createResourceRoute($action, $params, $multiple = true, $to = null) + { + $path = ''; + $pre_alias = []; + $resources = $this->resources; + $count = count($resources); + $inflector = Rails::services()->get('inflector'); + + $uncountable = ($action == 'index' || $action == 'create' || $action == 'new'); + $single = $count == 1; + + foreach ($resources as $k => $res) { + $path .= '/' . $res; + $last = $k + 1 == $count; + + if ($multiple) { + if ($uncountable && $last) { + continue; + } elseif ($last || $single) { + $path .= '/:id'; + } else { + $path .= '/:' . $inflector->singularize($res) . '_id'; + } + } else { + $singularRes = $inflector->singularize($res); + $pre_alias[] = $singularRes; + $path .= '/:' . $singularRes . '_id'; + } + + } + + if (!$multiple) { + $resources = $this->resource; + foreach ($resources as $res) { + $path .= '/' . $res; + } + } + + $path = ltrim($path, '/'); + + $setAlias = true; + switch ($action) { + case 'index': + $method = 'get'; + break; + + case 'new': + $method = 'get'; + $path .= '/new'; + $action = 'blank'; + array_unshift($pre_alias, 'new'); + break; + + case 'create': + $method = 'post'; + $setAlias = !$multiple; + break; + + case 'show': + $method = 'get'; + break; + + case 'edit': + $method = 'get'; + $path .= '/edit'; + array_unshift($pre_alias, 'edit'); + break; + + case 'update': + $method = ['put', 'patch']; + $setAlias = false; + break; + + case 'destroy': + $setAlias = false; + $method = 'delete'; + break; + } + + $controller = end($resources); + $plural = substr($controller, -1, 1) == 's'; + + if ($setAlias) { + if ($multiple) { + $aliasedResources = []; + foreach ($resources as $resource) { + $aliasedResources[] = $inflector->singularize($resource); + } + } else { + $aliasedResources = $resources; + } + + if ($action == 'index') { + $lastIndex = count($resources) - 1; + $aliasedResources[$lastIndex] = $resources[$lastIndex]; + } + + $pre_alias = implode('_', $pre_alias); + $pre_alias .= ($pre_alias && $this->routeNames ? '_' : '') . implode($this->routeNames); + $alias = ($pre_alias ? $pre_alias . '_' : '') . implode('_', $aliasedResources); + + if ($action == 'index') { + if (!$plural) { + $alias .= '_index'; + } + } + + // $alias = $inflector->camelize($alias, false); + + if (Route\RouteSet::validate_route_alias($alias)) { + Route\RouteSet::add_route_alias($alias); + $params['as'] = $alias; + } + } else { + $params['as'] = ''; + } + + if ($method) { + $params['via'] = $method; + } + + if (!$to) { + if (!$multiple) { + $controller = $inflector->pluralize($controller); + } + $to = $controller . '#' . $action; + } + + unset($params['on']); + + return $this->createRoute($path, $to, $params, ['skip_resources' => true]); + } + + private function createResourceNestedRoute($action, $params, $multiple = true, $to = null) + { + $path = ''; + $setAlias = true; + $resources = $this->resources; + + if (isset($params['as'])) { + $pre_alias = ''; + // $pre_alias = $params['as']; + $setAlias = false; + } elseif (preg_match('/\A\w+\Z/', $action)) { + $pre_alias = $action; + } else { + $pre_alias = ''; + $setAlias = false; + } + + foreach ($resources as $k => $res) { + $path .= '/' . $res; + + if ($this->createAsMember || (isset($params['on']) && $params['on'] == 'member')) + $path .= '/:id'; + else + $path .= '/:' . $res . '_id'; + + $pre_alias .= '_' . $res; + } + + if (!$multiple) { + $resources = $this->resource; + foreach ($resources as $res) { + $path .= '/' . $res; + } + } + + $method = !empty($params['via']) ? $params['via'] : null; + + + if ($action != 'index') { + $path .= '/' . $action; + } + + $path = ltrim($path, '/'); + + $controller = end($resources); + $plural = substr($controller, -1, 1) == 's'; + $inflector = Rails::services()->get('inflector'); + + if ($setAlias) { + $aliasedResources = []; + foreach ($resources as $resource) { + $aliasedResources[] = $inflector->singularize($resource); + } + + if ($action == 'index') { + $lastIndex = count($resources) - 1; + $aliasedResources[$lastIndex] = $resources[$lastIndex]; + } + + $pre_alias .= ($pre_alias && $this->routeNames ? '_' : '') . implode($this->routeNames); + $alias = ($pre_alias ? $pre_alias . '_' : '') . implode('_', $aliasedResources); + + if ($action == 'index') { + if (!$plural) + $alias .= '_index'; + } + // elseif ($plural) + // $alias = substr($alias, 0, -1); + + if (Route\RouteSet::validate_route_alias($alias)) { + Route\RouteSet::add_route_alias($alias); + $params['as'] = $alias; + } + } elseif (empty($params['as'])) { + $params['as'] = ''; + } + + if ($method) { + $params['via'] = $method; + } + + if (!$to) { + if (!$multiple) { + $controller = $inflector->pluralize($controller); + } + $to = $controller . '#' . $action; + } + + return $this->createRoute($path, $to, $params, ['skip_resources' => true]); + } + + /** + * Used by Rails. + */ + public function drawRoutes(Closure $block) + { + $block = $block->bindTo($this); + $block(); + + if (!$this->routeSet->rootRoute()) { + $this->root(self::ROOT_DEFAULT_TO); + } + + $this->createPanelRoute(); + $this->createAssetsRoute(); + } + + private function createRoute($url, $to, array $params = [], array $createOpts = []) + { + if (!array_key_exists('as', $params)) { + if (preg_match('/^[\w\/]+$/', $url)) { + $params['as'] = str_replace('/', '_', $url); + } + } + + $this->addNestedParams($url, $params); + + if (empty($createOpts['skip_resources']) && $this->resourceNesting) { + $current = end($this->resourceNesting); + $route = $this->createResourceNestedRoute($url, $params, $current == 'resources', $to); + } else { + unset($params['on']); + $route = new Route\Route($url, $to, $params); + } + return $route; + } + + private function addNestedParams(&$url, array &$params) + { + if ($this->scopeParams) { + $scopeParams = []; + foreach ($this->scopeParams as $k => $param) { + $scopeParams = array_merge($scopeParams, $param); + } + $params = array_merge($scopeParams, $params); + } + + if ($this->modules) { + if (!isset($params['module'])) { + $params['module'] = []; + } else { + $params['module'] = explode('/', $params['module']); + } + $params['modules'] = array_filter(array_merge($this->modules, $params['module'])); + unset($params['module']); + } + + if ($this->paths) { + if (!isset($params['path'])) { + $params['path'] = []; + } else { + $params['path'] = explode('/', $params['path']); + } + $params['paths'] = array_filter(array_merge(Toolbox\ArrayTools::flatten($this->paths), $params['path'])); + unset($params['path']); + } + } + + private function addRoute($route) + { + $route->build(); + $this->routeSet->add($route); + } + + private function createAndAddRoute($url, $to, array $params) + { + $this->addRoute($this->createRoute($url, $to, $params)); + } + + private function createRouteWithVerb($via, $url, $to = null, array $params) + { + if (is_array($to)) { + $params = $to; + $to = null; + } + $params['via'] = $via; + + $this->createAndAddRoute($url, $to, $params); + } + + private function createPanelRoute() + { + if ($panelPath = Rails::application()->config()->rails_panel_path) { + $route = new Route\PanelRoute( + $panelPath . '(/:action)', + self::ADMIN_DEFAULT_TO, + [ + 'rails_panel' => true, + 'defaults' => ['controller' => 'admin'], + 'via' => ['get', 'post'] + ] + ); + $route->build(); + $this->routeSet->set_panel_route($route); + + $this->routeSet->add(new Route\HiddenRoute($panelPath . '/stylesheet.css', + self::ADMIN_STYLESHEET_TO, + [ + 'via' => ['get'], + 'format' => false + ] + )); + } + } + + private function createAssetsRoute() + { + if (Rails::application()->config()->assets->enabled && !Rails::application()->config()->serve_static_assets) { + $prefix = ltrim(Rails::application()->config()->assets->prefix, '/'); + $this->routeSet->set_assets_route( + $this->createRoute( + $prefix . '/*file', + '#', + [ + 'assets_route' => true, + 'via' => ['get'], + 'format' => false + ] + ) + ); + } + } +} diff --git a/lib/Rails/Routing/Route/HiddenRoute.php b/lib/Rails/Routing/Route/HiddenRoute.php new file mode 100755 index 0000000..54aeff8 --- /dev/null +++ b/lib/Rails/Routing/Route/HiddenRoute.php @@ -0,0 +1,6 @@ + array( + 'constraint' => $regexp ?: null, + 'value' => $value ?: null, // Taken from request url or set by "default" + 'default' => $default ?: null, + 'is_optional'=> boolean, + 'preceded_by'=> . | / | null // This will help when building the route, only for optional vars + ) + */ + private $_vars = array(); + + private $_optional_groups; + + # variables in the order they're defined in the route. + private $_vars_names = array(); + + /** + * Variable values taken from the request url. + */ + private $_vars_values = array(); + + /** + * $var_name => $regexp + */ + private $_constraints = array(); + + private $_as; + + private $_subdomain; + + private $_paths = []; + + // private $_namespaces = []; + + private $_modules = []; + + private $_controller = ''; + + private $_action = ''; + + /** + * Following props serve for Build + */ + private $_optional_parts = array(); + + /** + * Needed to do a diff after building a route, + * to see which vars were actually used. + */ + private $_used_vars = array(); + + private $_build_url; + + private $_build_params; + + private + $_rails_panel = false, + $_assets_route = false; + + public function __construct($url, $to, array $params = array()) + { + foreach ($params as $name => $val) { + $prop = '_' . $name; + if (!property_exists($this, $prop)) + throw new Exception\RuntimeException( + sprintf("Tried to set invalid property '%s'.", $name) + ); + $this->$prop = $val; + } + + if (!$to && (is_bool(strpos($url, ':controller')) || is_bool(strpos($url, ':action')))) { + $parts = explode('/', $url); + $parts = array_slice($parts, 0, 2); + if (!isset($parts[1])) + $parts[1] = 'index'; + + $parts[1] = preg_split('/\W/', $parts[1], 2)[0]; + + $this->_to = implode('#', $parts); + } else + $this->_to = $to; + + # Set module. + if ($this->_modules && $this->_to) { + $this->_to = implode(UrlToken::MODULE_SEPARATOR, $this->_modules) . UrlToken::MODULE_SEPARATOR . $this->_to; + } + + if ($this->paths) { + $scope_paths = $this->paths; + $scope_paths = $scope_paths ? implode('/', $scope_paths) : ''; + $url && $url = '/' . $url; + $this->_url = $scope_paths . $url; + } else { + $this->_url = $url; + } + + if (!$this->_via) { + throw new Exception\RuntimeException( + sprintf( + "The 'via' option can't be empty (to=>%s)", + $this->_to + ) + ); + } + !is_array($this->_via) && $this->_via = array($this->_via); + } + + public function __get($prop) + { + $prop_prop = '_' . $prop; + if (!property_exists($this, $prop_prop)) + throw new Exception\RuntimeException( + sprintf("Property '%s' doesn't exist.", $prop) + ); + return $this->$prop_prop; + } + + public function build() + { + if (!$this->_escaped_url) { + $this->_check_format_var(); + $this->_extract_vars(); + $this->_escape_url(); + $this->_set_default_vars_values(); + $this->_set_alias(); + } + } + + public function match($url, $via) + { + if ($base_path = Rails::application()->router()->basePath()) + $url = substr($url, strlen($base_path)); + + $this->build(); + if ($this->_is_root()) + return $this->_match_root($url); + elseif ($this->rails_admin() && !Rails::application()->validateSafeIps()) + return false; + + if (!in_array(strtolower($via), $this->_via)) + return false; + + $url = ltrim($url, '/'); + $regex = '/^' . $this->_escaped_url . '$/'; + + if (!preg_match($regex, $url, $m)) + return false; + + array_shift($m); + + $var_names = array_keys($this->_vars); + + /** + * Add variable values to _var_values and also check if any + * of the matches values is also a property (like :format) + * to set it. + */ + foreach ($m as $k => $value) { + if (isset($this->_vars[$var_names[$k]]['constraint'])) { + if (substr($this->_vars[$var_names[$k]]['constraint'], 0, 1) == '/') { + if (!preg_match($this->_vars[$var_names[$k]]['constraint'], $value)) { + return false; + } + } else { + if ($value !== (string)$this->_vars[$var_names[$k]]['constraint']) + return false; + } + } + # Workaround for action: when "/post/", action will be "" and + # will cause errors; set it to "index". + if ($var_names[$k] == 'action') { + $value === "" && $value = 'index'; + $this->_action = $value; + } elseif ($var_names[$k] == 'format') { + $value === '' && $value = 'html'; + $this->_format = $value; + } + $this->_vars[$var_names[$k]]['value'] = $value; + } + + if (!$this->_assets_route) { + if (!$this->_parse_token()) + return false; + // elseif (!$this->_path_exists()) { + // throw new \Rails_ActionDispatch_Router_Route_Exception_ControllerFileNotFound(); + // throw new Exception\Rails_ActionDispatch_Router_Route_Exception_ControllerFileNotFound(); + // } + } + // vpe($this); + return true; + } + + public function match_with_token($token, array &$params) + { + if ($token === $this->_to || $token === $this->alias()) { + if ($url = $this->build_url($params, false)) { + return $url; + } + } elseif ($this->_has_variable_to()) { + $to = '/' . preg_replace('/:\w+/', '(\w+)', $this->_to) . '/'; + // $to = '/' . preg_replace('/:\w+/', '(\w+)', preg_quote($this->_to)) . '/'; + $type = $this->_parse_variable_to(); + + // try { + if (preg_match($to, $token, $m)) { + $params[$type] = $m[1]; + if ($url = $this->build_url($params, false)) { + return $url; + } + } + // } catch (\Exception $e) { + // vpe($this); + // } + } elseif (!$this->to) { + list($params['controller'], $params['action']) = explode('#', $token); + if ($url = $this->build_url($params, false)) { + return $url; + } + } + + return ''; + } + + /** + * Builds the URL with params passed and returns it. + * The params must include all variables defined for + * the route, and also must pass the regex validation, + * else an exception will be thrown. + * Any param that is not used by the route will be ignored. + * + * @return string the bare built url, with NO base_path. + */ + public function build_url(array $params, $raise_e = true) + { + // if ($this->_to == 'help#:action') + + $this->build(); + $this->_build_params = $params; + + $this->_build_url = $this->_url; + + # If main variables aren't present, return. + if (!$this->_check_main_vars()) { + $this->_build_params = array(); + $this->_build_url = ''; + return false; + } + if (($groups = $this->_optional_groups) && (array_shift($groups) ?: true) && $groups) { + foreach ($groups as $group) { + $parsed_group = $this->_check_optional_groups($group); + $this->_build_url = str_replace($group['part'], $parsed_group, $this->_build_url); + } + } + + # Remove remaining parentheses. + $this->_build_url = str_replace(array('(', ')'), '', $this->_build_url); + # Set build_params as its keys for later call of remaining_params() + $this->_build_params = array_keys($this->_build_params); + # Add leading slash as routes don't include it. + return '/' . $this->_build_url; + } + + public function build_url_path(array $params) + { + $build_params = []; + + // if (isset($params[0]) && $params[0] instanceof \Rails\ActiveRecord\Base) { + // $model = array_shift($params); + // } else + // $model = false; + $i = 0; + // vpe($params); + // $params = array_values($params); + foreach ($this->_vars as $name => $data) { + // if ($model) { + // $build_params[$name] = $model->$name; + // } else { + // vpe($params, !isset($params[$i]), empty($data['type'])); + if (!isset($params[$name]) || empty($data['type'])) + continue; + // vpe(is_array($params[$name])); + if (is_array($params[$name])) { + $build_params = array_merge($build_params, $params[$i]); + } else { + $build_params[$name] = $params[$name]; + } + // } + $i++; + } + + // vpe($build_params, $this->_vars); + // vpe($this); + $url = $this->build_url($build_params); + // vpe + // if (!$url) vpe($params, $build_params); + if ($this->_build_params) { + if ($query_params = array_filter(array_intersect_key($build_params, array_fill_keys($this->_build_params, true)))) + $url .= '?' . http_build_query($query_params); + } + + return $url; + } + + public function vars() + { + $vars = array(); + foreach ($this->_vars as $name => $params) { + $vars[$name] = $params['value']; + } + return $vars; + } + + public function remaining_params() + { + return array_fill_keys($this->_build_params, null); + } + + public function alias() + { + return $this->_as; + } + + public function to() + { + return $this->_to; + } + + public function url() + { + return $this->_url; + } + + public function via() + { + return $this->_via; + } + + public function namespaces() + { + return $this->_modules; + } + + public function modules() + { + return $this->_modules; + } + + public function controller() + { + return $this->_controller; + } + + public function action() + { + return $this->_action; + } + + public function rails_admin() + { + return $this instanceof PanelRoute; + // return $this->_rails_admin; + } + + public function isPanelRoute() + { + return $this instanceof PanelRoute; + } + + # TODO: Deprecated + public function panel_route() + { + return $this->isPanelRoute(); + } + + public function assets_route() + { + return $this->_assets_route; + } + + /** + * Builds the internal view path for the route out of + * module, controller and action. + * i.e. route for /post/show/15 would return + * this path: post/show[.$format] + * + * + * TODO: add path_names support. + */ + public function path(array $params = array()) + { + $this->build(); + + $path = []; + if ($this->_modules) + $path = $this->_modules; + $path[] = $this->_controller; + $path[] = isset($params['action']) ? $params['action'] : $this->_action; + + $format = isset($params['format']) ? '.' . $params['format'] : ''; + + return implode('/', $path) . $format; + } + + public function namespace_path() + { + if ($this->_modules) { + return implode('/', array_map(function($x) { return Rails::services()->get('inflector')->camelize($x); }, $this->_modules)); + } + } + + public function controller_file() + { + $path = Rails::config()->paths->controllers . '/' . (($namespacedPath = $this->namespace_path()) ? $namespacedPath . '/' : '') . Rails::services()->get('inflector')->camelize($this->controller) . 'Controller.php'; + return $path; + } + + /** + * Each namespace can have its own application controller. + * This is called by Rails_Application when checking for the file for ApplicationController; + * If we're in a namespace, the path to the application_controller file changes. + */ + public function namespaced_controller_file() + { + if (!$this->_modules) + return false; + return Rails::config()->paths->controllers . '/' . $this->namespace_path() . '/application_controller.php'; + } + + private function _check_main_vars() + { + if (($group = $this->_optional_groups) && ($group = array_shift($group))) { + foreach ($group as $mv) { + if (!isset($this->_build_params[$mv])) { + return false; + } else { + $this->_build_url = str_replace($this->_vars[$mv]['type'].$mv, $this->_build_params[$mv], $this->_build_url); + unset($this->_build_params[$mv]); + } + } + } + return true; + } + + private function _check_optional_groups($group) + { + # Used for workaround. + $action = false; + + $group_str = $part = $group['part']; + unset($group['part']); + + $main_vars = array_shift($group); + if (!$main_vars) + $group_str = ''; + + foreach ($main_vars as $mv) { + if (!isset($this->_vars[$mv])) + continue; + + if (!isset($this->_build_params[$mv])) { + $group_str = ''; + break; + } else { + # For workaround + if ($mv == 'action') + $action = $this->_build_params['action']; + $group_str = str_replace($this->_vars[$mv]['type'].$mv, $this->_build_params[$mv], $group_str); + unset($this->_build_params[$mv]); + } + } + + if ($group_str) { + foreach ($group as $subgroup) { + $subpart = $subgroup['part']; + $subrpl = $this->_check_optional_groups($subgroup); + # For workaround + if (!$subrpl) + array_shift($group); + $group_str = str_replace($subpart, $subrpl, $group_str); + } + } + /** + * Here comes the workaround. If the group has only one var + * and it's "action" and its value is "index" and there are no + * group memebers left (i.e., this current group is just something with + * 'index', remove it). + */ + if (count($main_vars) == 1 && $main_vars[0] == 'action' && $action == 'index' && !$group) { + $group_str = ''; + } + return $group_str; + } + + private function _match_root($url) + { + if ($url === $this->_root_url()) { + $this->_parse_token(); + return true; + } + return false; + } + + /** + * Doing this because of the options for the + * :format var, such as setting it to true or false. + * By default, it's always added by the end of the + * route as an optional var. + */ + private function _check_format_var() + { + if ($this->_format !== false && !$this->_is_root() && !preg_match('/(:|\*)format[^\w]?/', $this->_url)) { + if ($this->_format === true) + $this->_url .= '.:format'; + else + $this->_url .= '(.:format)'; + } + } + + private function _escape_url() + { + $this->_escaped_url = str_replace(array( + '(', + ')', + '.', + '/', + ), array( + '(?:', + ')?', + '\.', + '\/' + ), $this->_url); + + $repls = $subjs = array(); + + foreach ($this->_vars as $name => $var) { + if ($name == 'format') + $repl = '([a-zA-Z0-9]{1,5})'; + elseif ($var['type'] == '*') + $repl = '(.*?)'; + else + $repl = '([^\/]+?)'; + $repls[] = $repl; + $type = $var['type'] == '*' ? '\*' : $var['type']; + $subjs[] = '/' . $type . $name . '/'; + } + + $this->_escaped_url = preg_replace($subjs, $repls, $this->_escaped_url); + if (!$this->_assets_route) + $this->_escaped_url .= '\/?'; + } + + private function _find_vars($url_part = null) + { + $vars = array(); + if ($url_part === null) { + $url_part = $this->_url; + } else { + $vars['part'] = $url_part; + # Remove parentheses. + $url_part = substr($url_part, 1, -1); + } + + $parts = $this->_extract_groups($url_part); + $url_part = str_replace($parts, '%', $url_part); + + if (preg_match_all('/(\*|:)(\w+)/', $url_part, $ms)) { + foreach ($ms[1] as $k => $type) { + $var_name = $ms[2][$k]; + + # Get constraint from properties. + $constraint = isset($this->_constraints[$var_name]) ? $this->_constraints[$var_name] : null; + + # Get default from properties. + $default = isset($this->_defaults[$var_name]) ? $this->_defaults[$var_name] : null; + + $this->_vars_names[] = $var_name; + + $this->_vars[$var_name] = array( + 'type' => $type, + 'constraint' => $constraint, + 'default' => $default, + 'value' => null, + 'optional' => false + ); + } + $vars[] = $ms[2]; + } else + $vars[] = []; + + foreach ($parts as $subpart) { + if ($c = $this->_find_vars($subpart)) + $vars[] = $c; + } + return $vars; + } + + private function _extract_groups($str) + { + $parts = array(); + while ($part = $this->_extract_group($str)) { + $str = substr($str, strpos($str, $part) + strlen($part)); + $parts[] = $part; + } + return $parts; + } + + private function _extract_group($str) + { + if (false === ($pos = strpos($str, '('))) + return; + $group = ''; + $open = 1; + while ($open > 0) { + $str = substr($str, $pos+1); + $close_pos = strpos($str, ')'); + $part = substr($str, 0, $close_pos+1); + if (is_bool(strpos($part, '('))) { + $pos = $close_pos; + $group .= $part; + $open--; + } else { + $pos = strpos($str, '('); + $group .= substr($str, 0, $pos+1); + $open++; + } + } + return '(' . $group; + } + + /** + * Extracts var names and also replaces them in the regexp with their + * corresponding constraints. + */ + private function _extract_vars() + { + $this->_optional_groups = $this->_find_vars(); + $this->_constraints = []; + } + + /** + * Set index = action by default. + */ + private function _set_default_vars_values() + { + $default_action = 'index'; + + if (!$this->_assets_route) { + # Action defaulted to "index" even thought _to said + # otherwise. + if ($this->_to) { + try { + $token = new UrlToken($this->_to); + list($controller, $default_action) = $token->parts(); + } catch (Exception $e) { + } + } + } + + if (!isset($this->_defaults['action'])) + $this->_defaults['action'] = $default_action; + + foreach ($this->_defaults as $var => $val) { + if ($var == 'action') { + if ($val == ':action') { + $val = 'index'; + } + $this->_action = $val; + } elseif ($var == 'controller') + $this->_controller = $val; + $this->_vars[$var]['value'] = $val; + } + + if (!isset($this->_vars['format']['value']) || $this->_vars['format']['value'] === '') + $this->_vars['format']['value'] = 'html'; + $this->_format = $this->_vars['format']['value']; + + $this->_defaults = null; + } + + /** + * Check if the controller actually exists after + * matching the controller and action. + */ + // private function _path_exists() + // { + // if ($this->rails_admin()) + // return true; + // return is_file($this->controller_file()); + // } + + /** + * Parses the "to" property, which is actually a UrlToken. + * This isn't necessary to do if the route is like ':controller/:action'. + */ + private function _parse_token() + { + if (!$this->_controller || !$this->_action) { + if (!$this->_action && isset($this->_vars['action'])) + $this->_action = $this->_vars['action']['value']; + + if (!$this->_to) { + if (!isset($this->_vars['controller']) || !isset($this->_vars['action'])) + return false; + $this->_controller = $this->_vars['controller']['value']; + $this->_action = $this->_vars['action']['value']; + } else { + $to = $this->_has_variable_to() ? $this->_replace_variable_to($this->_controller, $this->_action) : $this->_to; + try { + $token = new UrlToken($to); + list($this->_controller, $this->_action) = $token->parts(); + } catch (Exception $e) { + return false; + } + } + } + return true; + } + + private function _is_root() + { + return $this->_url === $this->_root_url(); + } + + private function _root_url() + { + return Mapper::ROOT_URL; + } + + private function _replace_variable_to($controller, $action) + { + $parts = explode('#', $this->_to); + empty($parts[1]) && $parts[1] = 'index'; + + $part = is_int(strpos($parts[0], ':')) ? $parts[0] : $parts[1]; + + preg_match('/:(\w+)/', $part, $m); + $type = $m[1]; + return str_replace(':'.$type, $$type, $this->_to); + } + + /** + * Returns the variable part of the _to attribute. + */ + private function _parse_variable_to() + { + $parts = explode('#', $this->_to); + empty($parts[1]) && $parts[1] = 'index'; + + $part = is_int(strpos($parts[0], ':')) ? $parts[0] : $parts[1]; + + preg_match('/:(\w+)/', $part, $m); + $type = $m[1]; + + return $type; + } + + private function _set_alias() + { + if (!$this->_as) { + if ($this->_is_root()) + $this->_as = 'root'; + elseif ($this->rails_admin() && $this->_rails_panel) { + $this->_as = 'rails_panel'; + } + // elseif ($this->_controller && $this->_action) + // $this->_as = (($namespace = $this->_namespaces) ? implode('_', $namespace) . '_' : '') . $this->_controller . '_' . $this->_action; + } + } + + private function _has_variable_to() + { + return is_int(strpos($this->_to, ':')); + } +} \ No newline at end of file diff --git a/lib/Rails/Routing/Route/RouteSet.php b/lib/Rails/Routing/Route/RouteSet.php new file mode 100755 index 0000000..2c9578c --- /dev/null +++ b/lib/Rails/Routing/Route/RouteSet.php @@ -0,0 +1,94 @@ +routes = new ArrayObject(); + } + + public function getIterator() + { + return $this->routes; + } + + public function draw(\Closure $block) + { + if (!$this->routes_drawn) { + $mapper = new Mapper($this); + $mapper->drawRoutes($block); + $this->routes_drawn = true; + } + } + + public function add(Route $route) + { + $this->routes[] = $route; + } + + public function set_root_route(Route $route) + { + $this->rootRoute = $route; + } + + public function set_panel_route(Route $route) + { + $this->panel_route = $route; + } + + public function set_assets_route(Route $route) + { + $this->assets_route = $route; + } + + public function rootRoute() + { + return $this->rootRoute; + } + + public function panelRoute() + { + return $this->panel_route; + } + + public function assetsRoute() + { + return $this->assets_route; + } + + public function routes() + { + return $this->routes; + } +} \ No newline at end of file diff --git a/lib/Rails/Routing/Router.php b/lib/Rails/Routing/Router.php new file mode 100755 index 0000000..870d860 --- /dev/null +++ b/lib/Rails/Routing/Router.php @@ -0,0 +1,248 @@ +_url_helpers) + $this->_url_helpers = new UrlHelpers\UrlHelpers(); + return $this->_url_helpers; + } + + public function rootRoute() + { + return $this->routes()->rootRoute(); + } + + public function panelRoute() + { + return $this->routes()->panelRoute(); + } + + public function assetsRoute() + { + return $this->routes()->assetsRoute(); + } + + public function find_route() + { + $this->matchRoutes(); + } + + public function route() + { + return $this->_route; + } + + public function routes() + { + $this->importRoutes(); + return $this->_routes; + } + + /** + * Base path can be get in views by calling urlFor('base') or basePath(). + */ + public function basePath() + { + if ($this->base_path === false) { + if ($base_path = Rails::application()->config()->base_path) + $this->base_path = '/' . $base_path; + elseif (($base_path = $this->detectBasePath()) && $base_path != '/') + $this->base_path = $base_path; + else + $this->base_path = null; + } + return $this->base_path; + } + + public function rootPath() + { + ############ WARNING + ############ MUST UPDATE ALL SYSTEMS TO THIS CHANGE (added trailing slash when returning basePath) + if ($base_path = $this->basePath()) + return $base_path . '/'; + return '/'; + } + + public function urlFor($params, $ignore_missmatch = false) + { + $urlfor = new UrlFor($params, $ignore_missmatch); + return $urlfor->url(); + } + + private function importRoutes() + { + if ($this->_imported_routes) + return; + + $this->_imported_routes = true; + $this->_routes = new Route\RouteSet(); + + $config = Rails::config(); + $routes_file = $config->paths->config->concat('routes.php'); + + require $routes_file; + } + + private function matchRoutes() + { + $this->importRoutes(); + + $request_path = $this->_find_request_path(); + + $request_method = Rails::application()->dispatcher()->request()->method(); + + if ($this->rootRoute()->match($request_path, $request_method)) { + $this->_route = $this->rootRoute(); + } elseif ($this->panelRoute() && $this->panelRoute()->match($request_path, $request_method)) { + $this->_route = $this->panelRoute(); + } elseif ($this->assetsRoute() && $this->assetsRoute()->match($request_path, $request_method)) { + $this->_route = $this->assetsRoute(); + } else { + foreach ($this->routes() as $route) { + if ($route->match($request_path, $request_method)) { + if (!is_file($route->controller_file())) { + throw new Exception\RoutingErrorException( + sprintf('Controller file for "%sController" not found %s', + Rails::services()->get('inflector')->camelize($route->controller), + $route->modules ? ' [ module=>' . $route->namespace_path() . ' ]' : '' + ) + ); + } else { + $this->_route = $route; + break; + } + } + } + } + if (!$this->_route) { + throw new Exception\NotFoundException( + sprintf('No route matches [%s] "%s"', + Rails::application()->dispatcher()->request()->method(), + Rails::application()->dispatcher()->request()->path() + ) + ); + } + } + + private function _find_request_path() + { + if (!$this->_request_path) { + preg_match('/([^?]+)/', $_SERVER['REQUEST_URI'], $m); + $this->_request_path = $m[1]; + } + return $this->_request_path; + } + + /** + * This method was copied from ZF2, slightly modified to fit Rails. + * Zend\Http\PhpEnvironment\Request::detectBaseUrl() + */ + protected function detectBasePath() + { + $request = Rails::application()->dispatcher()->request(); + + $baseUrl = ''; + $filename = $request->get('SCRIPT_FILENAME'); + $scriptName = $request->get('SCRIPT_NAME'); + $phpSelf = $request->get('PHP_SELF'); + $origScriptName = $request->get('ORIG_SCRIPT_NAME'); + + if ($scriptName !== null && basename($scriptName) === $filename) { + $baseUrl = $scriptName; + } elseif ($phpSelf !== null && basename($phpSelf) === $filename) { + $baseUrl = $phpSelf; + } elseif ($origScriptName !== null && basename($origScriptName) === $filename) { + $baseUrl = $origScriptName; + } else { + $baseUrl = '/'; + $basename = basename($filename); + if ($basename) { + $path = ($phpSelf ? trim($phpSelf, '/') : ''); + $baseUrl .= substr($path, 0, strpos($path, $basename)) . $basename; + } + } + + $requestUri = $request->get('REQUEST_URI'); + + if (0 === strpos($requestUri, $baseUrl)) { + return $baseUrl; + } + + $baseDir = str_replace('\\', '/', dirname($baseUrl)); + if (0 === strpos($requestUri, $baseDir)) { + return $baseDir; + } + + $truncatedRequestUri = $requestUri; + + if (false !== ($pos = strpos($requestUri, '?'))) { + $truncatedRequestUri = substr($requestUri, 0, $pos); + } + + $basename = basename($baseUrl); + + if (empty($basename) || false === strpos($truncatedRequestUri, $basename)) { + return ''; + } + + if (strlen($requestUri) >= strlen($baseUrl) + && (false !== ($pos = strpos($requestUri, $baseUrl)) && $pos !== 0) + ) { + $baseUrl = substr($requestUri, 0, $pos + strlen($baseUrl)); + } + + return $baseUrl; + } +} \ No newline at end of file diff --git a/lib/Rails/Routing/Traits/NamedPathAwareTrait.php b/lib/Rails/Routing/Traits/NamedPathAwareTrait.php new file mode 100755 index 0000000..956f0b9 --- /dev/null +++ b/lib/Rails/Routing/Traits/NamedPathAwareTrait.php @@ -0,0 +1,24 @@ +get('inflector')->underscore(substr($method, 0, -4)); + + return \Rails::application() + ->router() + ->url_helpers() + ->find_route_with_alias($alias, $params); + } +} \ No newline at end of file diff --git a/lib/Rails/Routing/UrlFor.php b/lib/Rails/Routing/UrlFor.php new file mode 100755 index 0000000..7d933de --- /dev/null +++ b/lib/Rails/Routing/UrlFor.php @@ -0,0 +1,198 @@ +dispatcher()->router()->route()->controller . '#' . $params['action']; + else + $init_url = array_shift($params); + unset($params['controller'], $params['action']); + } else { + throw new Exception\InvalidArgumentException( + sprintf("Argument must be either string or array, %s passed", gettype($params)) + ); + } + + if (is_array($init_url)) { + $params = $init_url; + $init_url = array_shift($params); + } + + $this->_init_url = $init_url; + + # Set params. + if (isset($params['only_path'])) { + $this->_only_path = $params['only_path']; + unset($params['only_path']); + } + if (!empty($params['host'])) { + $this->_host = $params['host']; + unset($params['host']); + } + if (!empty($params['protocol'])) { + $this->_host = $params['protocol']; + unset($params['protocol']); + } + + $this->_params = $params; + + if ($init_url === 'root') { + $this->_url = $this->_base_url() ?: '/'; + } elseif ($init_url === 'rails_panel' && Rails::application()->config()->rails_panel_path) { + $this->_url = $this->_base_url() . '/' . Rails::application()->config()->rails_panel_path; + } elseif ($init_url == 'asset') { + $this->_url = Rails::assets()->prefix() . '/'; + } elseif ($init_url == 'base') { + $this->_url = Rails::application()->router()->basePath(); + } elseif (is_int(strpos($init_url, '#'))) { + $this->_parse_token($init_url); + $this->_find_route_for_token(); + + if ($this->_route) { + $this->_build_route_url(); + } else { + if (!$ignore_missmatch) { + # TODO: Why is this exception thrown here? + $modules = $this->_token->modules(); + if ($modules) { + $modules = implode(', ', $modules); + $modules = ' [ modules=>' . $modules . ' ]'; + } else { + $modules = ''; + } + $token = $this->_token->controller() . '#' . $this->_token->action(); + throw new Exception\RuntimeException( + sprintf('No route matches %s%s', $token, $modules) + ); + } else { + $this->_build_basic_url_for_token(); + } + } + } else { + $this->_url = $init_url; + if (strpos($this->_url, '/') === 0 && strlen($this->_url) > 1) + $this->_url = $this->_base_url() . $this->_url; + } + + $this->_build_params(); + $this->_build_url(); + + unset($this->_init_url, $this->_params, $this->_token, $this->_route, $this->_anchor); + } + + /** + * Returns final url. + */ + public function url() + { + return $this->_url; + } + + private function _parse_token($token) + { + if (Rails::application()->router()->route()) { + $namespaces = Rails::application()->router()->route()->namespaces(); + + # Automatically add the namespace if we're under one and none was set. + if ($namespaces && false === strpos($token, UrlToken::MODULE_SEPARATOR)) { + $token = implode(UrlToken::MODULE_SEPARATOR, $namespaces) . UrlToken::MODULE_SEPARATOR . $token; + } + } + $this->_token = new UrlToken($token); + } + + private function _find_route_for_token() + { + if ($data = Rails::application()->router()->url_helpers()->find_route_for_token($this->_token->toString(), $this->_params)) + list($this->_route, $this->_url) = $data; + } + + private function _build_basic_url_for_token() + { + $this->_url = $this->_base_url() . '/' . $this->_token->toUrl(); + } + + private function _build_route_url() + { + if (!$this->_url) + $this->_url = $this->_route->build_url($this->_params); + $this->_params = array_intersect_key($this->_params, $this->_route->remaining_params()); + } + + private function _build_params() + { + if (isset($this->_params['anchor'])) { + $this->_anchor = '#' . $this->_params['anchor']; + unset($this->_params['anchor']); + } + unset($this->_params['ignore_missmatch']); + $query = http_build_query($this->_params); + $this->_params = $query ? '?' . $query : ''; + } + + private function _build_url() + { + // if (strpos($this->_url, '/') === 0) + // $leading = $this->_base_url(); + // else + + + // $url = $leading . $this->_url . $this->_params . $this->_anchor; + $url = $this->_url . $this->_params . $this->_anchor; + + $this->_url = $url; + + if ($this->_host || !$this->_only_path) { + if ($this->_host) + $host = rtrim($this->_host, '/'); + else + $host = $_SERVER['SERVER_NAME']; + + $this->_url = $this->_protocol . '://' . $host . $this->_url; + } + } + + private function _base_url() + { + return Rails::application()->router()->basePath(); + } +} \ No newline at end of file diff --git a/lib/Rails/Routing/UrlHelpers/Exception/ExceptionInterface.php b/lib/Rails/Routing/UrlHelpers/Exception/ExceptionInterface.php new file mode 100755 index 0000000..b56e5af --- /dev/null +++ b/lib/Rails/Routing/UrlHelpers/Exception/ExceptionInterface.php @@ -0,0 +1,6 @@ +isNamedPathMethod($method)) { + return $this->getNamedPath($method, $params); + } + + throw new Exception\BadMethodCallException( + sprintf("Called to unknown method: %s", $method) + ); + } + + public function find_route_with_alias($alias, array $params = []) + { + if ($alias == 'root') { + return $this->router()->rootPath(); + } elseif ($alias == "base") { + return $this->router()->basePath(); + } elseif ($alias == "asset") { + return \Rails::assets()->prefix() . '/'; + } + + foreach ($this->router()->routes() as $route) { + if ($route->alias() == $alias) { + if (isset($params[0]) && $params[0] instanceof \Rails\ActiveRecord\Base) { + $params = $this->extract_route_vars_from_model($route, $params[0]); + } else { + $params = $this->assocWithRouteParams($route, $params); + } + $url = $route->build_url_path($params); + + if ($url) { + if ($base_path = $this->router()->basePath()) + $url = $base_path . $url; + + return $url; + } + } + } + // $msg = "No route found with alias '%s'"; + if ($params) { + $inlineParams = '( '; + foreach ($params as $k => $v) { + $inlineParams .= $k . '=>' . $v; + } + $inlineParams .= ' )'; + } else { + $inlineParams = "(no parameters)"; + } + throw new Exception\RuntimeException( + sprintf("No route found with alias '%s' %s", $alias, $inlineParams) + ); + } + + /** + * @return array, null | route and url + */ + public function find_route_for_token($token, $params = []) + { + if ($params instanceof \Rails\ActiveRecord\Base) + $model = $params; + else + $model = false; + + foreach ($this->router()->routes() as $route) { + if ($model) { + $params = $this->extract_route_vars_from_model($route, $model); + } + $url = $route->match_with_token($token, $params); + + if ($url) { + if ($base_path = $this->router()->basePath()) + $url = $base_path . $url; + return [$route, $url]; + } + } + } + + public function router() + { + return \Rails::application()->router(); + } + + private function extract_route_vars_from_model($route, $model) + { + $vars = []; + $modelClassName = get_class($model); + foreach (array_keys($route->vars()) as $name) { + // if (\Rails::config()->ar2) { + if ($modelClassName::isAttribute($name)) { + $vars[$name] = $model->$name; + } + // } else { + + // $vars[$name] = isset($model->$name) ? $model->$name : null; + // } + } + return $vars; + } + + private function assocWithRouteParams($route, $params) + { + $vars = []; + $params = array_values($params); + $i = 0; + foreach (array_keys($route->vars()) as $name) { + if (!isset($params[$i])) { + break; + } else { + $vars[$name] = $params[$i]; + $i++; + } + } + return $vars; + } +} \ No newline at end of file diff --git a/lib/Rails/Routing/UrlToken.php b/lib/Rails/Routing/UrlToken.php new file mode 100755 index 0000000..f0bb989 --- /dev/null +++ b/lib/Rails/Routing/UrlToken.php @@ -0,0 +1,89 @@ +modules = array_filter(explode(self::MODULE_SEPARATOR, $namespace)); + } + } + + if (is_bool(strpos($token, self::SEPARATOR))) + throw new Exception\InvalidArgumentException(sprintf("Missing separator in token '%s'", $token)); + $parts = explode(self::SEPARATOR, $token); + + if (empty($parts[0])) { + $route = Rails::application()->dispatcher()->router()->route(); + if (!$route) { + throw new Exception\RuntimeException( + "Can't complete URL token as there's no active route" + ); + } + $this->modules = $route->modules(); + $parts[0] = $route->controller(); + } + $this->controller = $parts[0]; + + if (!empty($parts[1])) + $this->action = $parts[1]; + } + + public function parts() + { + return array($this->controller, $this->action, $this->modules); + } + + public function controller() + { + return $this->controller; + } + + public function action() + { + return $this->action; + } + + public function namespaces() + { + return $this->modules; + } + + public function modules() + { + return $this->modules; + } + + public function token() + { + return $this->toString(); + } + + public function toString() + { + $namespace = $this->modules ? implode(self::MODULE_SEPARATOR, $this->modules) . self::MODULE_SEPARATOR : ''; + return $namespace . $this->controller . self::SEPARATOR . $this->action; + } + + public function toUrl() + { + return str_replace(['#', self::MODULE_SEPARATOR], '/', $this->toString()); + } +} diff --git a/lib/Rails/ServiceManager/Exception/ExceptionInterface.php b/lib/Rails/ServiceManager/Exception/ExceptionInterface.php new file mode 100755 index 0000000..2e1427b --- /dev/null +++ b/lib/Rails/ServiceManager/Exception/ExceptionInterface.php @@ -0,0 +1,6 @@ +get($name); + } +} diff --git a/lib/Rails/ServiceManager/ServiceManager.php b/lib/Rails/ServiceManager/ServiceManager.php new file mode 100755 index 0000000..936c933 --- /dev/null +++ b/lib/Rails/ServiceManager/ServiceManager.php @@ -0,0 +1,45 @@ +serviceList = [ + 'inflector' => [ + 'class_name' => 'Rails\ActiveSupport\Inflector\Inflector' + ], + 'i18n' => [ + 'class_name' => 'Rails\I18n\I18n' + ], + 'rails.cache' => function() { + $cache = new \Rails\Cache\Cache('file'); + } + ]; + } + + public function get($name) + { + if (!isset($this->instances[$name])) { + if (isset($this->serviceList[$name])) { + if ($this->serviceList[$name] instanceof \Closure) { + $this->instance[$name] = $this->serviceList[$name](); + } else { + $this->instances[$name] = new $this->serviceList[$name]['class_name']; + } + } else { + throw new Exception\RuntimeException( + sprintf("Unknown service %s", $name) + ); + } + } + return $this->instances[$name]; + } +} \ No newline at end of file diff --git a/lib/Rails/SystemExit/SystemExit.php b/lib/Rails/SystemExit/SystemExit.php new file mode 100755 index 0000000..d69b36f --- /dev/null +++ b/lib/Rails/SystemExit/SystemExit.php @@ -0,0 +1,32 @@ +callbacks[$name] = $callback; + else + $this->callbacks[] = $callback; + } + + public function unregister($name) + { + if (isset($this->callbacks[$name])) { + unset($this->callbacks[$name]; + return true; + } else { + return false; + } + } + + public function run() + { + foreach ($this->callbacks as $callback) { + call_user_func($callback); + } + } +} \ No newline at end of file diff --git a/lib/Rails/Toolbox/ArrayTools.php b/lib/Rails/Toolbox/ArrayTools.php new file mode 100755 index 0000000..d490a7a --- /dev/null +++ b/lib/Rails/Toolbox/ArrayTools.php @@ -0,0 +1,34 @@ +getParentClass(); + if (!$parent) { + break; + } else { + $parents[] = $parent->getName(); + } + } + return $parents; + } +} \ No newline at end of file diff --git a/lib/Rails/Toolbox/DbTools.php b/lib/Rails/Toolbox/DbTools.php new file mode 100755 index 0000000..786dff0 --- /dev/null +++ b/lib/Rails/Toolbox/DbTools.php @@ -0,0 +1,53 @@ + $cdata) { + if (!isset($cdata['username']) || !isset($cdata['password']) || !isset($cdata['database'])) { + continue; + } else { + try { + ActiveRecord::setConnection($connectionName); + $connection = ActiveRecord::connection(); + + $dbname = $connection->selectValue("SELECT DATABASE()"); + $tables = $connection->selectValues(sprintf('SHOW TABLES FROM `%s`', $dbname)); + } catch (\Exception $e) { + continue; + } + + if (!$tables) { + throw new Exception\RuntimeException( + sprintf( + 'Couldn\'t retrieve table information for connection %s', + $connectionName + ) + ); + } + + foreach ($tables as $table_name) { + $class = 'Rails\ActiveRecord\Adapter\\' . $connection->adapterName() . '\Table'; + $data = $class::fetchSchema($connection, $table_name); + + $path = \Rails::root() . '/db/table_schema/' . $connection->name(); + if (!is_dir($path)) + mkdir($path, 0777, true); + + $file = $path . '/' . $table_name . '.php'; + + $contents = "paths->controllers; + + $fileName = $name . 'Controller.php'; + + preg_match('~(.*?)' . self::NAMESPACE_SEPARATOR . '(\w+)~', $name, $m); + + if (!empty($m[1])) { + $fileParts = explode(self::NAMESPACE_SEPARATOR, $m[1]); + $folders = implode(DIRECTORY_SEPARATOR, $fileParts); + $folderPath = $baseDir . DIRECTORY_SEPARATOR . $folders; + + if (!is_dir($folderPath)) { + mkdir($folderPath, 0755, true); + } + + $namespace = "\nnamespace " . $m[1] . ";\n"; + $className = $m[2]; + + $filePath = $folderPath . DIRECTORY_SEPARATOR . $m[2] . '.php'; + } else { + $namespace = ''; + $className = $name; + $filePath = $baseDir . DIRECTORY_SEPARATOR . $fileName; + } + + if (is_file($filePath)) { + $message = sprintf("File already exists (pass 'f' to overwrite): %s", $filePath); + if ($console) { + $console->terminate($message); + } else { + throw new Exception\FileExistsException( + $message + ); + } + } + + $defaultOptions = [ + 'parent' => '' + ]; + + $options = array_merge($defaultOptions, $options); + + if (!$options['parent'] || $options['parent'] == 'namespaced') { + $parent = 'ApplicationController'; + + if ($options['parent'] != 'namespaced' && $namespace) { + $parent = '\\' . $parent; + } + } else { + $parent = "\n" . $options['parent']; + } + + $template = self::template(); + + $contents = str_replace([ + '%namespace%', + '%className%', + '%parent%', + ], [ + $namespace, + $className, + $parent, + ], $template); + + if (!file_put_contents($filePath, $contents)) { + $msg = "Couldn't create file"; + if ($console) { + $console->terminate($msg); + } else { + throw new Exception\FileNotCreatedException( + $msg + ); + } + } + + if ($console) { + $console->terminate("Created file: " . $filePath); + } else { + return true; + } + } + + static private function template() + { + return 'paths->models; + + $fileName = $name . '.php'; + + preg_match('~(.*?)' . self::NAMESPACE_SEPARATOR . '(\w+)~', $name, $m); + + if (!empty($m[1])) { + $fileParts = explode(self::NAMESPACE_SEPARATOR, $m[1]); + $folders = implode(DIRECTORY_SEPARATOR, $fileParts); + $folderPath = $modelsDir . DIRECTORY_SEPARATOR . $folders; + + if (!is_dir($folderPath)) { + mkdir($folderPath, 0755, true); + } + + $namespace = "\nnamespace " . $m[1] . ";\n"; + $className = $m[2]; + + $filePath = $folderPath . DIRECTORY_SEPARATOR . $m[2] . '.php'; + } else { + $namespace = ''; + $className = $name; + $filePath = $modelsDir . DIRECTORY_SEPARATOR . $fileName; + } + + if (is_file($filePath)) { + $message = sprintf("File already exists (pass 'f' to overwrite): %s", $filePath); + if ($console) { + $console->terminate($message); + } else { + throw new Exception\FileExistsException( + $message + ); + } + } + + $defaultOptions = [ + 'parent' => '' + ]; + + $options = array_merge($defaultOptions, $options); + + if (!$options['parent']) { + $parent = 'Rails\ActiveRecord\Base'; + + if ($namespace) { + $parent = '\\' . $parent; + } + } else { + $parent = "\n" . $options['parent']; + } + + $template = self::modelTemplate(); + + $contents = str_replace([ + '%namespace%', + '%className%', + '%parent%', + ], [ + $namespace, + $className, + $parent, + ], $template); + + if (!file_put_contents($filePath, $contents)) { + $msg = "Couldn't create file"; + if ($console) { + $console->terminate($msg); + } else { + throw new Exception\FileNotCreatedException( + $msg + ); + } + } + + if ($console) { + $console->terminate("Created file: " . $filePath); + } else { + return true; + } + } + + static private function modelTemplate() + { + return 'isFile() ? unlink($path->getPathname()) : rmdir($path->getPathname()); + } + } + + /** + * Lists all directories and subdirectories found in a path. + */ + static public function listDirs($root) + { + $dirs = []; + foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::CHILD_FIRST) as $path) { + if ($path->isDir()) { + $dirs[] = $path->getPathname(); + } + } + return $dirs; + } + + /** + * Search for all files matching a pattern within a directory and sub directories. + */ + static public function searchFile($root, $pattern = '*', $flags = 0) + { + $dirs = self::listDirs($root); + array_unshift($dirs, $root); + + $foundFiles = []; + + foreach ($dirs as $dir) { + $filePatt = $dir . '/' . $pattern; + $files = glob($filePatt, $flags); + + if ($files) { + $foundFiles = array_merge($foundFiles, $files); + } + } + + return $foundFiles; + } + + // static public function mod_time($path) + // { + // return self::modTime($path); + // } + + /** + * File modification time. + * Found at PHP.net + * filemtime() returns invalid date on Windows, this function fixes that. + */ + static public function modTime($path) + { + $time = filemtime($path); + $is_dst = (date('I', $time) == 1); + $system_dst = (date('I') == 1); + $adjustment = 0; + + if($is_dst == false && $system_dst == true) + $adjustment = 3600; + elseif($is_dst == true && $system_dst == false) + $adjustment = -3600; + else + $adjustment = 0; + + return ($time + $adjustment); + } +} \ No newline at end of file diff --git a/lib/Rails/Toolbox/functions.php b/lib/Rails/Toolbox/functions.php new file mode 100755 index 0000000..8a9b2d3 --- /dev/null +++ b/lib/Rails/Toolbox/functions.php @@ -0,0 +1,25 @@ +'; + $vars = func_get_args(); + call_user_func_array('var_dump', $vars); + echo ''; +} + +function vde() { + $vars = func_get_args(); + call_user_func_array('var_dump', $vars); + exit; +} + +function vpe() { + echo '
';
+    call_user_func_array('vd', func_get_args());
+    echo '
'; + exit; +} \ No newline at end of file diff --git a/lib/Rails/Traits/ArrayObjectAccess.php b/lib/Rails/Traits/ArrayObjectAccess.php new file mode 100755 index 0000000..0eb0e7c --- /dev/null +++ b/lib/Rails/Traits/ArrayObjectAccess.php @@ -0,0 +1,69 @@ +headers('foo', 'bar') : Sets $headers['foo'] = 'bar'. + * $object->headers('foo') : Returns the value of $headers['foo'], null if not set. + * $object->headers('foo', null) : Unsets $headers['foo']. + * $object->headers() : Returns $headers. + * $object->headers(['foo' => 'bar', 'baz' => true]) : Sets many values at once. + * + * The method to access the $headers property should look like this: + * + * public function headers() + * { + * return $this->_aoaccess('headers', func_get_args()); + * } + */ +trait ArrayObjectAccess +{ + private function _aoaccess($prop_name, $args) + { + $num_args = count($args); + + if ($num_args == 1) { + $key = array_shift($args); + if (is_array($key)) { + foreach ($key as $prop => $val) + $this->_aoaccess_set($prop_name, $prop, $val); + return $this; + } elseif (isset($this->$prop_name[$key])) + return $this->$prop_name[$key]; + else + return null; + } elseif ($num_args) { + list($key, $value) = $args; + $this->_aoaccess_set($prop_name, $key, $value); + return $this; + } else { + return $this->$prop_name; + } + } + + /** + * Helps automatize the creation of ArrayObjects. + * Expected to be called in the constructor. + * + * @param array $props : Names of the properties that are ArrayObjects. + */ + private function _aoaccess_init(array $props) + { + foreach ($props as $prop) + $this->$prop = new \ArrayObject(); + } + + private function _aoaccess_set($prop_name, $key, $value) + { + if ($value === null) + unset($this->$prop_name[$key]); + else + $this->$prop_name[$key] = $value; + } +} \ No newline at end of file diff --git a/lib/Rails/Validation/Exception/ExceptionInterface.php b/lib/Rails/Validation/Exception/ExceptionInterface.php new file mode 100755 index 0000000..c8e5e3a --- /dev/null +++ b/lib/Rails/Validation/Exception/ExceptionInterface.php @@ -0,0 +1,6 @@ +_type = $type; + $this->_data = $data; + $this->_params = $params; + } + + public function validate() + { + $validate_method = '_validate_' . $this->_type; + if (!method_exists($this, $validate_method)) { + throw new Exception\RuntimeException( + sprintf("Validation for '%s' isn't supported", $this->_type) + ); + } + $this->_success = $this->$validate_method(); + return $this; + } + + public function success() + { + return $this->_success; + } + + protected function _validate_length() + { + $this->_data = strlen($this->_data); + $this->_params['only_integer'] = true; + return $this->_validate_number(); + } + + protected function _validate_format() + { + return (bool)preg_match($this->_params['with'], $this->_data); + } + + protected function _validate_number() + { + $result = 2; + $this->_convert_number($this->_data); + + if (!empty($this->_params['in'])) { + list($min, $max) = $this->_params['in']; + + if ($this->_data < $min) + $result = -1; + elseif ($this->_data > $max) + $result = 1; + } elseif (!empty($this->_params['is'])) { + if ((int)$this->_data != (int)$this->_params['is']) { + if ($this->_data > $this->_params['is']) { + $result = 1; + } else { + $result = -1; + } + } + } elseif (!empty($this->_params['minimum'])) { + if ((int)$this->_data < (int)$this->_params['minimum']) { + $result = -1; + } + } elseif (!empty($this->_params['maximum'])) { + if ((int)$this->_data > (int)$this->_params['maximum']) { + $result = 1; + } + } else { + throw new Exception\RuntimeException( + "No supported number validation passed" + ); + } + + # Check params. { + if (!empty($this->_params['even'])) { + if (($this->_data % 2)) + $success = false; + } elseif (!empty($this->_params['odd'])) { + if (($this->_data % 1)) + $success = false; + } + # } + + if (isset($success) && $success === false) { + $this->_result = $result; + } elseif ($result !== 2) { + $success = false; + $this->_result = $result; + } else + $success = true; + + return $success; + } + + protected function _validate_inclusion() + { + if (is_array($this->_rule)) { + if (!in_array($this->_data, $this->_rule)) + return false; + } elseif (is_string($this->_rule) && is_int(strpos($this->_rule, '..'))) { + return $this->_validate_number(); + } else { + throw new Exception\InvalidArgumentException( + sprintf("Invalid exclusion validation rule, must be either an array or a numeric rule, %s passed", gettype($this->_rule)) + ); + } + } + + protected function _validate_exclusion() + { + if (is_array($this->_rule)) { + if (in_array($this->_data, $this->_rule)) + return false; + } elseif (is_string($this->_rule) && is_int(strpos($this->_rule, '..'))) { + return !($this->_validate_number()); + } else { + throw new Exception\InvalidArgumentException( + sprintf("Invalid exclusion validation rule, must be either an array or a numeric rule, %s passed", gettype($this->_rule)) + ); + } + } + + /** + * Helper function for _validate_number() + */ + private function _convert_number(&$num) + { + if (!empty($this->_params['only_integer'])) + $num = (int)$num; + else + $num = (float)$num; + } +} \ No newline at end of file diff --git a/lib/Rails/Xml/Exception/ExceptionInterface.php b/lib/Rails/Xml/Exception/ExceptionInterface.php new file mode 100755 index 0000000..e958589 --- /dev/null +++ b/lib/Rails/Xml/Exception/ExceptionInterface.php @@ -0,0 +1,6 @@ +toXml(); + } elseif (is_array($el)) { + $attrs = $el; + } else { + throw new Exception\InvalidArgumentException( + sprintf("%s accepts either a child of ActiveRecord\Base or an array, %s passed", + __METHOD__, gettype($attrs)) + ); + } + + if (!isset($params['root'])) + throw new Exception\InvalidArgumentException( + sprintf('InvalidArgumentException', "Missing 'root' parameter for %s", __METHOD__) + ); + + $this->_attrs = $attrs; + $this->_root = $params['root']; + unset($params['root']); + + $this->_params = $params; + } + + public function instruct() + { + $this->_buffer .= ''."\n"; + } + + public function create() + { + if (empty($this->_params['skip_instruct'])) + $this->instruct(); + + $this->_buffer .= '<'.$this->_root; + + $attrs_str = []; + + # TODO: fix $val, it should accept any value. + foreach ($this->_attrs as $name => $val) { + if (is_bool($val)) + $val = $val ? 'true' : 'false'; + elseif (!is_scalar($val)) + $val = ''; + + $attrs_str[] = $name . '="'.htmlspecialchars((string)$val).'"'; + } + + $this->_buffer .= ' ' . implode(' ', $attrs_str); + + $this->_buffer .= ' />'; + } + + public function output() + { + if (!$this->_buffer) + $this->create(); + return $this->_buffer; + } +} \ No newline at end of file diff --git a/lib/Rails/Yaml/Parser.php b/lib/Rails/Yaml/Parser.php new file mode 100755 index 0000000..ffd1eea --- /dev/null +++ b/lib/Rails/Yaml/Parser.php @@ -0,0 +1,59 @@ +read(); + } + + static public function writeFile($filepath, $contents) + { + return (new self($filepath))->write($contents); + } + + public function __construct($filepath) + { + $this->filepath = $filepath; + } + + public function read() + { + try { + if (function_exists('yaml_parse')) { + return yaml_parse_file($this->filepath); + } else { + return SfYaml::parse($this->filepath); + } + } catch (\Exception $e) { + $msg = sprintf("Error while reading file %s:\n", $this->filepath); + $msg .= $e->getMessage(); + $cn = get_class($e); + throw new $cn($msg); + } + } + + public function write($contents, array $params = []) + { + try { + if (function_exists('yaml_emit')) { + $params = array_merge([$contents], $params); + $yaml = call_user_func_array('yaml_emit', $params); + return file_put_contents($this->filepath, $yaml); + } else { + $yaml = call_user_func_array('Symfony\Component\Yaml\Yaml::dump', array_merge([$contents], $params)); + return file_put_contents($this->filepath, $yaml); + } + } catch (\Exception $e) { + $msg = sprintf("Error while writing file %s:\n", $this->filepath); + $msg .= $e->getMessage(); + $cn = get_class($e); + throw new $cn($msg); + } + } +} \ No newline at end of file diff --git a/vendor/README b/vendor/README new file mode 100755 index 0000000..7c5019f --- /dev/null +++ b/vendor/README @@ -0,0 +1 @@ +This assets folder is used by the Rails Panel. \ No newline at end of file diff --git a/vendor/assets/stylesheets/bootstrap.css b/vendor/assets/stylesheets/bootstrap.css new file mode 100755 index 0000000..4049691 --- /dev/null +++ b/vendor/assets/stylesheets/bootstrap.css @@ -0,0 +1,6805 @@ +/*! + * Bootstrap v3.0.0 + * + * Copyright 2013 Twitter, Inc + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Designed and built with all the love in the world by @mdo and @fat. + */ + +/*! normalize.css v2.1.0 | MIT License | git.io/normalize */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +nav, +section, +summary { + display: block; +} + +audio, +canvas, +video { + display: inline-block; +} + +audio:not([controls]) { + display: none; + height: 0; +} + +[hidden] { + display: none; +} + +html { + font-family: sans-serif; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + margin: 0; +} + +a:focus { + outline: thin dotted; +} + +a:active, +a:hover { + outline: 0; +} + +h1 { + margin: 0.67em 0; + font-size: 2em; +} + +abbr[title] { + border-bottom: 1px dotted; +} + +b, +strong { + font-weight: bold; +} + +dfn { + font-style: italic; +} + +hr { + height: 0; + -moz-box-sizing: content-box; + box-sizing: content-box; +} + +mark { + color: #000; + background: #ff0; +} + +code, +kbd, +pre, +samp { + font-family: monospace, serif; + font-size: 1em; +} + +pre { + white-space: pre-wrap; +} + +q { + quotes: "\201C" "\201D" "\2018" "\2019"; +} + +small { + font-size: 80%; +} + +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +img { + border: 0; +} + +svg:not(:root) { + overflow: hidden; +} + +figure { + margin: 0; +} + +fieldset { + padding: 0.35em 0.625em 0.75em; + margin: 0 2px; + border: 1px solid #c0c0c0; +} + +legend { + padding: 0; + border: 0; +} + +button, +input, +select, +textarea { + margin: 0; + font-family: inherit; + font-size: 100%; +} + +button, +input { + line-height: normal; +} + +button, +select { + text-transform: none; +} + +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + cursor: pointer; + -webkit-appearance: button; +} + +button[disabled], +html input[disabled] { + cursor: default; +} + +input[type="checkbox"], +input[type="radio"] { + padding: 0; + box-sizing: border-box; +} + +input[type="search"] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} + +textarea { + overflow: auto; + vertical-align: top; +} + +table { + border-collapse: collapse; + border-spacing: 0; +} + +@media print { + * { + color: #000 !important; + text-shadow: none !important; + background: transparent !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + .ir a:after, + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + @page { + margin: 2cm .5cm; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + .navbar { + display: none; + } + .table td, + .table th { + background-color: #fff !important; + } + .btn > .caret, + .dropup > .btn > .caret { + border-top-color: #000 !important; + } + .label { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; + } +} + +*, +*:before, +*:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +html { + font-size: 62.5%; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} + +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.428571429; + color: #333333; + background-color: #ffffff; +} + +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} + +button, +input, +select[multiple], +textarea { + background-image: none; +} + +a { + color: #428bca; + text-decoration: none; +} + +a:hover, +a:focus { + color: #2a6496; + text-decoration: underline; +} + +a:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +img { + vertical-align: middle; +} + +.img-responsive { + display: block; + height: auto; + max-width: 100%; +} + +.img-rounded { + border-radius: 6px; +} + +.img-thumbnail { + display: inline-block; + height: auto; + max-width: 100%; + padding: 4px; + line-height: 1.428571429; + background-color: #ffffff; + border: 1px solid #dddddd; + border-radius: 4px; + -webkit-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} + +.img-circle { + border-radius: 50%; +} + +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #eeeeee; +} + +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0 0 0 0); + border: 0; +} + +p { + margin: 0 0 10px; +} + +.lead { + margin-bottom: 20px; + font-size: 16.099999999999998px; + font-weight: 200; + line-height: 1.4; +} + +@media (min-width: 768px) { + .lead { + font-size: 21px; + } +} + +small { + font-size: 85%; +} + +cite { + font-style: normal; +} + +.text-muted { + color: #999999; +} + +.text-primary { + color: #428bca; +} + +.text-warning { + color: #c09853; +} + +.text-danger { + color: #b94a48; +} + +.text-success { + color: #468847; +} + +.text-info { + color: #3a87ad; +} + +.text-left { + text-align: left; +} + +.text-right { + text-align: right; +} + +.text-center { + text-align: center; +} + +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-weight: 500; + line-height: 1.1; +} + +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small, +.h1 small, +.h2 small, +.h3 small, +.h4 small, +.h5 small, +.h6 small { + font-weight: normal; + line-height: 1; + color: #999999; +} + +h1, +h2, +h3 { + margin-top: 20px; + margin-bottom: 10px; +} + +h4, +h5, +h6 { + margin-top: 10px; + margin-bottom: 10px; +} + +h1, +.h1 { + font-size: 36px; +} + +h2, +.h2 { + font-size: 30px; +} + +h3, +.h3 { + font-size: 24px; +} + +h4, +.h4 { + font-size: 18px; +} + +h5, +.h5 { + font-size: 14px; +} + +h6, +.h6 { + font-size: 12px; +} + +h1 small, +.h1 small { + font-size: 24px; +} + +h2 small, +.h2 small { + font-size: 18px; +} + +h3 small, +.h3 small, +h4 small, +.h4 small { + font-size: 14px; +} + +.page-header { + padding-bottom: 9px; + margin: 40px 0 20px; + border-bottom: 1px solid #eeeeee; +} + +ul, +ol { + margin-top: 0; + margin-bottom: 10px; +} + +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} + +.list-unstyled { + padding-left: 0; + list-style: none; +} + +.list-inline { + padding-left: 0; + list-style: none; +} + +.list-inline > li { + display: inline-block; + padding-right: 5px; + padding-left: 5px; +} + +dl { + margin-bottom: 20px; +} + +dt, +dd { + line-height: 1.428571429; +} + +dt { + font-weight: bold; +} + +dd { + margin-left: 0; +} + +@media (min-width: 768px) { + .dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + } + .dl-horizontal dd { + margin-left: 180px; + } + .dl-horizontal dd:before, + .dl-horizontal dd:after { + display: table; + content: " "; + } + .dl-horizontal dd:after { + clear: both; + } + .dl-horizontal dd:before, + .dl-horizontal dd:after { + display: table; + content: " "; + } + .dl-horizontal dd:after { + clear: both; + } +} + +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #999999; +} + +abbr.initialism { + font-size: 90%; + text-transform: uppercase; +} + +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + border-left: 5px solid #eeeeee; +} + +blockquote p { + font-size: 17.5px; + font-weight: 300; + line-height: 1.25; +} + +blockquote p:last-child { + margin-bottom: 0; +} + +blockquote small { + display: block; + line-height: 1.428571429; + color: #999999; +} + +blockquote small:before { + content: '\2014 \00A0'; +} + +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + border-right: 5px solid #eeeeee; + border-left: 0; +} + +blockquote.pull-right p, +blockquote.pull-right small { + text-align: right; +} + +blockquote.pull-right small:before { + content: ''; +} + +blockquote.pull-right small:after { + content: '\00A0 \2014'; +} + +q:before, +q:after, +blockquote:before, +blockquote:after { + content: ""; +} + +address { + display: block; + margin-bottom: 20px; + font-style: normal; + line-height: 1.428571429; +} + +code, +pre { + font-family: Monaco, Menlo, Consolas, "Courier New", monospace; +} + +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + white-space: nowrap; + background-color: #f9f2f4; + border-radius: 4px; +} + +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.428571429; + color: #333333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #cccccc; + border-radius: 4px; +} + +pre.prettyprint { + margin-bottom: 20px; +} + +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border: 0; +} + +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} + +.container { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} + +.container:before, +.container:after { + display: table; + content: " "; +} + +.container:after { + clear: both; +} + +.container:before, +.container:after { + display: table; + content: " "; +} + +.container:after { + clear: both; +} + +.row { + margin-right: -15px; + margin-left: -15px; +} + +.row:before, +.row:after { + display: table; + content: " "; +} + +.row:after { + clear: both; +} + +.row:before, +.row:after { + display: table; + content: " "; +} + +.row:after { + clear: both; +} + +.col-xs-1, +.col-xs-2, +.col-xs-3, +.col-xs-4, +.col-xs-5, +.col-xs-6, +.col-xs-7, +.col-xs-8, +.col-xs-9, +.col-xs-10, +.col-xs-11, +.col-xs-12, +.col-sm-1, +.col-sm-2, +.col-sm-3, +.col-sm-4, +.col-sm-5, +.col-sm-6, +.col-sm-7, +.col-sm-8, +.col-sm-9, +.col-sm-10, +.col-sm-11, +.col-sm-12, +.col-md-1, +.col-md-2, +.col-md-3, +.col-md-4, +.col-md-5, +.col-md-6, +.col-md-7, +.col-md-8, +.col-md-9, +.col-md-10, +.col-md-11, +.col-md-12, +.col-lg-1, +.col-lg-2, +.col-lg-3, +.col-lg-4, +.col-lg-5, +.col-lg-6, +.col-lg-7, +.col-lg-8, +.col-lg-9, +.col-lg-10, +.col-lg-11, +.col-lg-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} + +.col-xs-1, +.col-xs-2, +.col-xs-3, +.col-xs-4, +.col-xs-5, +.col-xs-6, +.col-xs-7, +.col-xs-8, +.col-xs-9, +.col-xs-10, +.col-xs-11 { + float: left; +} + +.col-xs-1 { + width: 8.333333333333332%; +} + +.col-xs-2 { + width: 16.666666666666664%; +} + +.col-xs-3 { + width: 25%; +} + +.col-xs-4 { + width: 33.33333333333333%; +} + +.col-xs-5 { + width: 41.66666666666667%; +} + +.col-xs-6 { + width: 50%; +} + +.col-xs-7 { + width: 58.333333333333336%; +} + +.col-xs-8 { + width: 66.66666666666666%; +} + +.col-xs-9 { + width: 75%; +} + +.col-xs-10 { + width: 83.33333333333334%; +} + +.col-xs-11 { + width: 91.66666666666666%; +} + +.col-xs-12 { + width: 100%; +} + +@media (min-width: 768px) { + .container { + max-width: 750px; + } + .col-sm-1, + .col-sm-2, + .col-sm-3, + .col-sm-4, + .col-sm-5, + .col-sm-6, + .col-sm-7, + .col-sm-8, + .col-sm-9, + .col-sm-10, + .col-sm-11 { + float: left; + } + .col-sm-1 { + width: 8.333333333333332%; + } + .col-sm-2 { + width: 16.666666666666664%; + } + .col-sm-3 { + width: 25%; + } + .col-sm-4 { + width: 33.33333333333333%; + } + .col-sm-5 { + width: 41.66666666666667%; + } + .col-sm-6 { + width: 50%; + } + .col-sm-7 { + width: 58.333333333333336%; + } + .col-sm-8 { + width: 66.66666666666666%; + } + .col-sm-9 { + width: 75%; + } + .col-sm-10 { + width: 83.33333333333334%; + } + .col-sm-11 { + width: 91.66666666666666%; + } + .col-sm-12 { + width: 100%; + } + .col-sm-push-1 { + left: 8.333333333333332%; + } + .col-sm-push-2 { + left: 16.666666666666664%; + } + .col-sm-push-3 { + left: 25%; + } + .col-sm-push-4 { + left: 33.33333333333333%; + } + .col-sm-push-5 { + left: 41.66666666666667%; + } + .col-sm-push-6 { + left: 50%; + } + .col-sm-push-7 { + left: 58.333333333333336%; + } + .col-sm-push-8 { + left: 66.66666666666666%; + } + .col-sm-push-9 { + left: 75%; + } + .col-sm-push-10 { + left: 83.33333333333334%; + } + .col-sm-push-11 { + left: 91.66666666666666%; + } + .col-sm-pull-1 { + right: 8.333333333333332%; + } + .col-sm-pull-2 { + right: 16.666666666666664%; + } + .col-sm-pull-3 { + right: 25%; + } + .col-sm-pull-4 { + right: 33.33333333333333%; + } + .col-sm-pull-5 { + right: 41.66666666666667%; + } + .col-sm-pull-6 { + right: 50%; + } + .col-sm-pull-7 { + right: 58.333333333333336%; + } + .col-sm-pull-8 { + right: 66.66666666666666%; + } + .col-sm-pull-9 { + right: 75%; + } + .col-sm-pull-10 { + right: 83.33333333333334%; + } + .col-sm-pull-11 { + right: 91.66666666666666%; + } + .col-sm-offset-1 { + margin-left: 8.333333333333332%; + } + .col-sm-offset-2 { + margin-left: 16.666666666666664%; + } + .col-sm-offset-3 { + margin-left: 25%; + } + .col-sm-offset-4 { + margin-left: 33.33333333333333%; + } + .col-sm-offset-5 { + margin-left: 41.66666666666667%; + } + .col-sm-offset-6 { + margin-left: 50%; + } + .col-sm-offset-7 { + margin-left: 58.333333333333336%; + } + .col-sm-offset-8 { + margin-left: 66.66666666666666%; + } + .col-sm-offset-9 { + margin-left: 75%; + } + .col-sm-offset-10 { + margin-left: 83.33333333333334%; + } + .col-sm-offset-11 { + margin-left: 91.66666666666666%; + } +} + +@media (min-width: 992px) { + .container { + max-width: 970px; + } + .col-md-1, + .col-md-2, + .col-md-3, + .col-md-4, + .col-md-5, + .col-md-6, + .col-md-7, + .col-md-8, + .col-md-9, + .col-md-10, + .col-md-11 { + float: left; + } + .col-md-1 { + width: 8.333333333333332%; + } + .col-md-2 { + width: 16.666666666666664%; + } + .col-md-3 { + width: 25%; + } + .col-md-4 { + width: 33.33333333333333%; + } + .col-md-5 { + width: 41.66666666666667%; + } + .col-md-6 { + width: 50%; + } + .col-md-7 { + width: 58.333333333333336%; + } + .col-md-8 { + width: 66.66666666666666%; + } + .col-md-9 { + width: 75%; + } + .col-md-10 { + width: 83.33333333333334%; + } + .col-md-11 { + width: 91.66666666666666%; + } + .col-md-12 { + width: 100%; + } + .col-md-push-0 { + left: auto; + } + .col-md-push-1 { + left: 8.333333333333332%; + } + .col-md-push-2 { + left: 16.666666666666664%; + } + .col-md-push-3 { + left: 25%; + } + .col-md-push-4 { + left: 33.33333333333333%; + } + .col-md-push-5 { + left: 41.66666666666667%; + } + .col-md-push-6 { + left: 50%; + } + .col-md-push-7 { + left: 58.333333333333336%; + } + .col-md-push-8 { + left: 66.66666666666666%; + } + .col-md-push-9 { + left: 75%; + } + .col-md-push-10 { + left: 83.33333333333334%; + } + .col-md-push-11 { + left: 91.66666666666666%; + } + .col-md-pull-0 { + right: auto; + } + .col-md-pull-1 { + right: 8.333333333333332%; + } + .col-md-pull-2 { + right: 16.666666666666664%; + } + .col-md-pull-3 { + right: 25%; + } + .col-md-pull-4 { + right: 33.33333333333333%; + } + .col-md-pull-5 { + right: 41.66666666666667%; + } + .col-md-pull-6 { + right: 50%; + } + .col-md-pull-7 { + right: 58.333333333333336%; + } + .col-md-pull-8 { + right: 66.66666666666666%; + } + .col-md-pull-9 { + right: 75%; + } + .col-md-pull-10 { + right: 83.33333333333334%; + } + .col-md-pull-11 { + right: 91.66666666666666%; + } + .col-md-offset-0 { + margin-left: 0; + } + .col-md-offset-1 { + margin-left: 8.333333333333332%; + } + .col-md-offset-2 { + margin-left: 16.666666666666664%; + } + .col-md-offset-3 { + margin-left: 25%; + } + .col-md-offset-4 { + margin-left: 33.33333333333333%; + } + .col-md-offset-5 { + margin-left: 41.66666666666667%; + } + .col-md-offset-6 { + margin-left: 50%; + } + .col-md-offset-7 { + margin-left: 58.333333333333336%; + } + .col-md-offset-8 { + margin-left: 66.66666666666666%; + } + .col-md-offset-9 { + margin-left: 75%; + } + .col-md-offset-10 { + margin-left: 83.33333333333334%; + } + .col-md-offset-11 { + margin-left: 91.66666666666666%; + } +} + +@media (min-width: 1200px) { + .container { + max-width: 1170px; + } + .col-lg-1, + .col-lg-2, + .col-lg-3, + .col-lg-4, + .col-lg-5, + .col-lg-6, + .col-lg-7, + .col-lg-8, + .col-lg-9, + .col-lg-10, + .col-lg-11 { + float: left; + } + .col-lg-1 { + width: 8.333333333333332%; + } + .col-lg-2 { + width: 16.666666666666664%; + } + .col-lg-3 { + width: 25%; + } + .col-lg-4 { + width: 33.33333333333333%; + } + .col-lg-5 { + width: 41.66666666666667%; + } + .col-lg-6 { + width: 50%; + } + .col-lg-7 { + width: 58.333333333333336%; + } + .col-lg-8 { + width: 66.66666666666666%; + } + .col-lg-9 { + width: 75%; + } + .col-lg-10 { + width: 83.33333333333334%; + } + .col-lg-11 { + width: 91.66666666666666%; + } + .col-lg-12 { + width: 100%; + } + .col-lg-push-0 { + left: auto; + } + .col-lg-push-1 { + left: 8.333333333333332%; + } + .col-lg-push-2 { + left: 16.666666666666664%; + } + .col-lg-push-3 { + left: 25%; + } + .col-lg-push-4 { + left: 33.33333333333333%; + } + .col-lg-push-5 { + left: 41.66666666666667%; + } + .col-lg-push-6 { + left: 50%; + } + .col-lg-push-7 { + left: 58.333333333333336%; + } + .col-lg-push-8 { + left: 66.66666666666666%; + } + .col-lg-push-9 { + left: 75%; + } + .col-lg-push-10 { + left: 83.33333333333334%; + } + .col-lg-push-11 { + left: 91.66666666666666%; + } + .col-lg-pull-0 { + right: auto; + } + .col-lg-pull-1 { + right: 8.333333333333332%; + } + .col-lg-pull-2 { + right: 16.666666666666664%; + } + .col-lg-pull-3 { + right: 25%; + } + .col-lg-pull-4 { + right: 33.33333333333333%; + } + .col-lg-pull-5 { + right: 41.66666666666667%; + } + .col-lg-pull-6 { + right: 50%; + } + .col-lg-pull-7 { + right: 58.333333333333336%; + } + .col-lg-pull-8 { + right: 66.66666666666666%; + } + .col-lg-pull-9 { + right: 75%; + } + .col-lg-pull-10 { + right: 83.33333333333334%; + } + .col-lg-pull-11 { + right: 91.66666666666666%; + } + .col-lg-offset-0 { + margin-left: 0; + } + .col-lg-offset-1 { + margin-left: 8.333333333333332%; + } + .col-lg-offset-2 { + margin-left: 16.666666666666664%; + } + .col-lg-offset-3 { + margin-left: 25%; + } + .col-lg-offset-4 { + margin-left: 33.33333333333333%; + } + .col-lg-offset-5 { + margin-left: 41.66666666666667%; + } + .col-lg-offset-6 { + margin-left: 50%; + } + .col-lg-offset-7 { + margin-left: 58.333333333333336%; + } + .col-lg-offset-8 { + margin-left: 66.66666666666666%; + } + .col-lg-offset-9 { + margin-left: 75%; + } + .col-lg-offset-10 { + margin-left: 83.33333333333334%; + } + .col-lg-offset-11 { + margin-left: 91.66666666666666%; + } +} + +table { + max-width: 100%; + background-color: transparent; +} + +th { + text-align: left; +} + +.table { + width: 100%; + margin-bottom: 20px; +} + +.table thead > tr > th, +.table tbody > tr > th, +.table tfoot > tr > th, +.table thead > tr > td, +.table tbody > tr > td, +.table tfoot > tr > td { + padding: 8px; + line-height: 1.428571429; + vertical-align: top; + border-top: 1px solid #dddddd; +} + +.table thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #dddddd; +} + +.table caption + thead tr:first-child th, +.table colgroup + thead tr:first-child th, +.table thead:first-child tr:first-child th, +.table caption + thead tr:first-child td, +.table colgroup + thead tr:first-child td, +.table thead:first-child tr:first-child td { + border-top: 0; +} + +.table tbody + tbody { + border-top: 2px solid #dddddd; +} + +.table .table { + background-color: #ffffff; +} + +.table-condensed thead > tr > th, +.table-condensed tbody > tr > th, +.table-condensed tfoot > tr > th, +.table-condensed thead > tr > td, +.table-condensed tbody > tr > td, +.table-condensed tfoot > tr > td { + padding: 5px; +} + +.table-bordered { + border: 1px solid #dddddd; +} + +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #dddddd; +} + +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 2px; +} + +.table-striped > tbody > tr:nth-child(odd) > td, +.table-striped > tbody > tr:nth-child(odd) > th { + background-color: #f9f9f9; +} + +.table-hover > tbody > tr:hover > td, +.table-hover > tbody > tr:hover > th { + background-color: #f5f5f5; +} + +table col[class*="col-"] { + display: table-column; + float: none; +} + +table td[class*="col-"], +table th[class*="col-"] { + display: table-cell; + float: none; +} + +.table > thead > tr > td.active, +.table > tbody > tr > td.active, +.table > tfoot > tr > td.active, +.table > thead > tr > th.active, +.table > tbody > tr > th.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > tbody > tr.active > td, +.table > tfoot > tr.active > td, +.table > thead > tr.active > th, +.table > tbody > tr.active > th, +.table > tfoot > tr.active > th { + background-color: #f5f5f5; +} + +.table > thead > tr > td.success, +.table > tbody > tr > td.success, +.table > tfoot > tr > td.success, +.table > thead > tr > th.success, +.table > tbody > tr > th.success, +.table > tfoot > tr > th.success, +.table > thead > tr.success > td, +.table > tbody > tr.success > td, +.table > tfoot > tr.success > td, +.table > thead > tr.success > th, +.table > tbody > tr.success > th, +.table > tfoot > tr.success > th { + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, +.table-hover > tbody > tr.success:hover > td { + background-color: #d0e9c6; + border-color: #c9e2b3; +} + +.table > thead > tr > td.danger, +.table > tbody > tr > td.danger, +.table > tfoot > tr > td.danger, +.table > thead > tr > th.danger, +.table > tbody > tr > th.danger, +.table > tfoot > tr > th.danger, +.table > thead > tr.danger > td, +.table > tbody > tr.danger > td, +.table > tfoot > tr.danger > td, +.table > thead > tr.danger > th, +.table > tbody > tr.danger > th, +.table > tfoot > tr.danger > th { + background-color: #f2dede; + border-color: #eed3d7; +} + +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover, +.table-hover > tbody > tr.danger:hover > td { + background-color: #ebcccc; + border-color: #e6c1c7; +} + +.table > thead > tr > td.warning, +.table > tbody > tr > td.warning, +.table > tfoot > tr > td.warning, +.table > thead > tr > th.warning, +.table > tbody > tr > th.warning, +.table > tfoot > tr > th.warning, +.table > thead > tr.warning > td, +.table > tbody > tr.warning > td, +.table > tfoot > tr.warning > td, +.table > thead > tr.warning > th, +.table > tbody > tr.warning > th, +.table > tfoot > tr.warning > th { + background-color: #fcf8e3; + border-color: #fbeed5; +} + +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover, +.table-hover > tbody > tr.warning:hover > td { + background-color: #faf2cc; + border-color: #f8e5be; +} + +@media (max-width: 768px) { + .table-responsive { + width: 100%; + margin-bottom: 15px; + overflow-x: scroll; + overflow-y: hidden; + border: 1px solid #dddddd; + } + .table-responsive > .table { + margin-bottom: 0; + background-color: #fff; + } + .table-responsive > .table > thead > tr > th, + .table-responsive > .table > tbody > tr > th, + .table-responsive > .table > tfoot > tr > th, + .table-responsive > .table > thead > tr > td, + .table-responsive > .table > tbody > tr > td, + .table-responsive > .table > tfoot > tr > td { + white-space: nowrap; + } + .table-responsive > .table-bordered { + border: 0; + } + .table-responsive > .table-bordered > thead > tr > th:first-child, + .table-responsive > .table-bordered > tbody > tr > th:first-child, + .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .table-responsive > .table-bordered > thead > tr > td:first-child, + .table-responsive > .table-bordered > tbody > tr > td:first-child, + .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; + } + .table-responsive > .table-bordered > thead > tr > th:last-child, + .table-responsive > .table-bordered > tbody > tr > th:last-child, + .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .table-responsive > .table-bordered > thead > tr > td:last-child, + .table-responsive > .table-bordered > tbody > tr > td:last-child, + .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; + } + .table-responsive > .table-bordered > thead > tr:last-child > th, + .table-responsive > .table-bordered > tbody > tr:last-child > th, + .table-responsive > .table-bordered > tfoot > tr:last-child > th, + .table-responsive > .table-bordered > thead > tr:last-child > td, + .table-responsive > .table-bordered > tbody > tr:last-child > td, + .table-responsive > .table-bordered > tfoot > tr:last-child > td { + border-bottom: 0; + } +} + +fieldset { + padding: 0; + margin: 0; + border: 0; +} + +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: inherit; + color: #333333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} + +label { + display: inline-block; + margin-bottom: 5px; + font-weight: bold; +} + +input[type="search"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} + +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + /* IE8-9 */ + + line-height: normal; +} + +input[type="file"] { + display: block; +} + +select[multiple], +select[size] { + height: auto; +} + +select optgroup { + font-family: inherit; + font-size: inherit; + font-style: inherit; +} + +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +input[type="number"]::-webkit-outer-spin-button, +input[type="number"]::-webkit-inner-spin-button { + height: auto; +} + +.form-control:-moz-placeholder { + color: #999999; +} + +.form-control::-moz-placeholder { + color: #999999; +} + +.form-control:-ms-input-placeholder { + color: #999999; +} + +.form-control::-webkit-input-placeholder { + color: #999999; +} + +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.428571429; + color: #555555; + vertical-align: middle; + background-color: #ffffff; + border: 1px solid #cccccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -webkit-transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; + transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; +} + +.form-control:focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); +} + +.form-control[disabled], +.form-control[readonly], +fieldset[disabled] .form-control { + cursor: not-allowed; + background-color: #eeeeee; +} + +textarea.form-control { + height: auto; +} + +.form-group { + margin-bottom: 15px; +} + +.radio, +.checkbox { + display: block; + min-height: 20px; + padding-left: 20px; + margin-top: 10px; + margin-bottom: 10px; + vertical-align: middle; +} + +.radio label, +.checkbox label { + display: inline; + margin-bottom: 0; + font-weight: normal; + cursor: pointer; +} + +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + float: left; + margin-left: -20px; +} + +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; +} + +.radio-inline, +.checkbox-inline { + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + vertical-align: middle; + cursor: pointer; +} + +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; +} + +input[type="radio"][disabled], +input[type="checkbox"][disabled], +.radio[disabled], +.radio-inline[disabled], +.checkbox[disabled], +.checkbox-inline[disabled], +fieldset[disabled] input[type="radio"], +fieldset[disabled] input[type="checkbox"], +fieldset[disabled] .radio, +fieldset[disabled] .radio-inline, +fieldset[disabled] .checkbox, +fieldset[disabled] .checkbox-inline { + cursor: not-allowed; +} + +.input-sm { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +select.input-sm { + height: 30px; + line-height: 30px; +} + +textarea.input-sm { + height: auto; +} + +.input-lg { + height: 45px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} + +select.input-lg { + height: 45px; + line-height: 45px; +} + +textarea.input-lg { + height: auto; +} + +.has-warning .help-block, +.has-warning .control-label { + color: #c09853; +} + +.has-warning .form-control { + border-color: #c09853; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.has-warning .form-control:focus { + border-color: #a47e3c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #dbc59e; +} + +.has-warning .input-group-addon { + color: #c09853; + background-color: #fcf8e3; + border-color: #c09853; +} + +.has-error .help-block, +.has-error .control-label { + color: #b94a48; +} + +.has-error .form-control { + border-color: #b94a48; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.has-error .form-control:focus { + border-color: #953b39; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #d59392; +} + +.has-error .input-group-addon { + color: #b94a48; + background-color: #f2dede; + border-color: #b94a48; +} + +.has-success .help-block, +.has-success .control-label { + color: #468847; +} + +.has-success .form-control { + border-color: #468847; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.has-success .form-control:focus { + border-color: #356635; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #7aba7b; +} + +.has-success .input-group-addon { + color: #468847; + background-color: #dff0d8; + border-color: #468847; +} + +.form-control-static { + padding-top: 7px; + margin-bottom: 0; +} + +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #737373; +} + +@media (min-width: 768px) { + .form-inline .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .form-control { + display: inline-block; + } + .form-inline .radio, + .form-inline .checkbox { + display: inline-block; + padding-left: 0; + margin-top: 0; + margin-bottom: 0; + } + .form-inline .radio input[type="radio"], + .form-inline .checkbox input[type="checkbox"] { + float: none; + margin-left: 0; + } +} + +.form-horizontal .control-label, +.form-horizontal .radio, +.form-horizontal .checkbox, +.form-horizontal .radio-inline, +.form-horizontal .checkbox-inline { + padding-top: 7px; + margin-top: 0; + margin-bottom: 0; +} + +.form-horizontal .form-group { + margin-right: -15px; + margin-left: -15px; +} + +.form-horizontal .form-group:before, +.form-horizontal .form-group:after { + display: table; + content: " "; +} + +.form-horizontal .form-group:after { + clear: both; +} + +.form-horizontal .form-group:before, +.form-horizontal .form-group:after { + display: table; + content: " "; +} + +.form-horizontal .form-group:after { + clear: both; +} + +@media (min-width: 768px) { + .form-horizontal .control-label { + text-align: right; + } +} + +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.428571429; + text-align: center; + white-space: nowrap; + vertical-align: middle; + cursor: pointer; + border: 1px solid transparent; + border-radius: 4px; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + -o-user-select: none; + user-select: none; +} + +.btn:focus { + outline: thin dotted #333; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} + +.btn:hover, +.btn:focus { + color: #333333; + text-decoration: none; +} + +.btn:active, +.btn.active { + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} + +.btn.disabled, +.btn[disabled], +fieldset[disabled] .btn { + pointer-events: none; + cursor: not-allowed; + opacity: 0.65; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + box-shadow: none; +} + +.btn-default { + color: #333333; + background-color: #ffffff; + border-color: #cccccc; +} + +.btn-default:hover, +.btn-default:focus, +.btn-default:active, +.btn-default.active, +.open .dropdown-toggle.btn-default { + color: #333333; + background-color: #ebebeb; + border-color: #adadad; +} + +.btn-default:active, +.btn-default.active, +.open .dropdown-toggle.btn-default { + background-image: none; +} + +.btn-default.disabled, +.btn-default[disabled], +fieldset[disabled] .btn-default, +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled:active, +.btn-default[disabled]:active, +fieldset[disabled] .btn-default:active, +.btn-default.disabled.active, +.btn-default[disabled].active, +fieldset[disabled] .btn-default.active { + background-color: #ffffff; + border-color: #cccccc; +} + +.btn-primary { + color: #ffffff; + background-color: #428bca; + border-color: #357ebd; +} + +.btn-primary:hover, +.btn-primary:focus, +.btn-primary:active, +.btn-primary.active, +.open .dropdown-toggle.btn-primary { + color: #ffffff; + background-color: #3276b1; + border-color: #285e8e; +} + +.btn-primary:active, +.btn-primary.active, +.open .dropdown-toggle.btn-primary { + background-image: none; +} + +.btn-primary.disabled, +.btn-primary[disabled], +fieldset[disabled] .btn-primary, +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled:active, +.btn-primary[disabled]:active, +fieldset[disabled] .btn-primary:active, +.btn-primary.disabled.active, +.btn-primary[disabled].active, +fieldset[disabled] .btn-primary.active { + background-color: #428bca; + border-color: #357ebd; +} + +.btn-warning { + color: #ffffff; + background-color: #f0ad4e; + border-color: #eea236; +} + +.btn-warning:hover, +.btn-warning:focus, +.btn-warning:active, +.btn-warning.active, +.open .dropdown-toggle.btn-warning { + color: #ffffff; + background-color: #ed9c28; + border-color: #d58512; +} + +.btn-warning:active, +.btn-warning.active, +.open .dropdown-toggle.btn-warning { + background-image: none; +} + +.btn-warning.disabled, +.btn-warning[disabled], +fieldset[disabled] .btn-warning, +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled:active, +.btn-warning[disabled]:active, +fieldset[disabled] .btn-warning:active, +.btn-warning.disabled.active, +.btn-warning[disabled].active, +fieldset[disabled] .btn-warning.active { + background-color: #f0ad4e; + border-color: #eea236; +} + +.btn-danger { + color: #ffffff; + background-color: #d9534f; + border-color: #d43f3a; +} + +.btn-danger:hover, +.btn-danger:focus, +.btn-danger:active, +.btn-danger.active, +.open .dropdown-toggle.btn-danger { + color: #ffffff; + background-color: #d2322d; + border-color: #ac2925; +} + +.btn-danger:active, +.btn-danger.active, +.open .dropdown-toggle.btn-danger { + background-image: none; +} + +.btn-danger.disabled, +.btn-danger[disabled], +fieldset[disabled] .btn-danger, +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled:active, +.btn-danger[disabled]:active, +fieldset[disabled] .btn-danger:active, +.btn-danger.disabled.active, +.btn-danger[disabled].active, +fieldset[disabled] .btn-danger.active { + background-color: #d9534f; + border-color: #d43f3a; +} + +.btn-success { + color: #ffffff; + background-color: #5cb85c; + border-color: #4cae4c; +} + +.btn-success:hover, +.btn-success:focus, +.btn-success:active, +.btn-success.active, +.open .dropdown-toggle.btn-success { + color: #ffffff; + background-color: #47a447; + border-color: #398439; +} + +.btn-success:active, +.btn-success.active, +.open .dropdown-toggle.btn-success { + background-image: none; +} + +.btn-success.disabled, +.btn-success[disabled], +fieldset[disabled] .btn-success, +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled:active, +.btn-success[disabled]:active, +fieldset[disabled] .btn-success:active, +.btn-success.disabled.active, +.btn-success[disabled].active, +fieldset[disabled] .btn-success.active { + background-color: #5cb85c; + border-color: #4cae4c; +} + +.btn-info { + color: #ffffff; + background-color: #5bc0de; + border-color: #46b8da; +} + +.btn-info:hover, +.btn-info:focus, +.btn-info:active, +.btn-info.active, +.open .dropdown-toggle.btn-info { + color: #ffffff; + background-color: #39b3d7; + border-color: #269abc; +} + +.btn-info:active, +.btn-info.active, +.open .dropdown-toggle.btn-info { + background-image: none; +} + +.btn-info.disabled, +.btn-info[disabled], +fieldset[disabled] .btn-info, +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled:active, +.btn-info[disabled]:active, +fieldset[disabled] .btn-info:active, +.btn-info.disabled.active, +.btn-info[disabled].active, +fieldset[disabled] .btn-info.active { + background-color: #5bc0de; + border-color: #46b8da; +} + +.btn-link { + font-weight: normal; + color: #428bca; + cursor: pointer; + border-radius: 0; +} + +.btn-link, +.btn-link:active, +.btn-link[disabled], +fieldset[disabled] .btn-link { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} + +.btn-link, +.btn-link:hover, +.btn-link:focus, +.btn-link:active { + border-color: transparent; +} + +.btn-link:hover, +.btn-link:focus { + color: #2a6496; + text-decoration: underline; + background-color: transparent; +} + +.btn-link[disabled]:hover, +fieldset[disabled] .btn-link:hover, +.btn-link[disabled]:focus, +fieldset[disabled] .btn-link:focus { + color: #999999; + text-decoration: none; +} + +.btn-lg { + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} + +.btn-sm, +.btn-xs { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +.btn-xs { + padding: 1px 5px; +} + +.btn-block { + display: block; + width: 100%; + padding-right: 0; + padding-left: 0; +} + +.btn-block + .btn-block { + margin-top: 5px; +} + +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} + +.fade { + opacity: 0; + -webkit-transition: opacity 0.15s linear; + transition: opacity 0.15s linear; +} + +.fade.in { + opacity: 1; +} + +.collapse { + display: none; +} + +.collapse.in { + display: block; +} + +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition: height 0.35s ease; + transition: height 0.35s ease; +} + +@font-face { + font-family: 'Glyphicons Halflings'; + src: url('glyphicons-halflings-regular.eot'); + src: url('glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('glyphicons-halflings-regular.woff') format('woff'), url('glyphicons-halflings-regular.ttf') format('truetype'), url('glyphicons-halflings-regular.svg#glyphicons-halflingsregular') format('svg'); +} + +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + -webkit-font-smoothing: antialiased; + font-style: normal; + font-weight: normal; + line-height: 1; +} + +.glyphicon-asterisk:before { + content: "\2a"; +} + +.glyphicon-plus:before { + content: "\2b"; +} + +.glyphicon-euro:before { + content: "\20ac"; +} + +.glyphicon-minus:before { + content: "\2212"; +} + +.glyphicon-cloud:before { + content: "\2601"; +} + +.glyphicon-envelope:before { + content: "\2709"; +} + +.glyphicon-pencil:before { + content: "\270f"; +} + +.glyphicon-glass:before { + content: "\e001"; +} + +.glyphicon-music:before { + content: "\e002"; +} + +.glyphicon-search:before { + content: "\e003"; +} + +.glyphicon-heart:before { + content: "\e005"; +} + +.glyphicon-star:before { + content: "\e006"; +} + +.glyphicon-star-empty:before { + content: "\e007"; +} + +.glyphicon-user:before { + content: "\e008"; +} + +.glyphicon-film:before { + content: "\e009"; +} + +.glyphicon-th-large:before { + content: "\e010"; +} + +.glyphicon-th:before { + content: "\e011"; +} + +.glyphicon-th-list:before { + content: "\e012"; +} + +.glyphicon-ok:before { + content: "\e013"; +} + +.glyphicon-remove:before { + content: "\e014"; +} + +.glyphicon-zoom-in:before { + content: "\e015"; +} + +.glyphicon-zoom-out:before { + content: "\e016"; +} + +.glyphicon-off:before { + content: "\e017"; +} + +.glyphicon-signal:before { + content: "\e018"; +} + +.glyphicon-cog:before { + content: "\e019"; +} + +.glyphicon-trash:before { + content: "\e020"; +} + +.glyphicon-home:before { + content: "\e021"; +} + +.glyphicon-file:before { + content: "\e022"; +} + +.glyphicon-time:before { + content: "\e023"; +} + +.glyphicon-road:before { + content: "\e024"; +} + +.glyphicon-download-alt:before { + content: "\e025"; +} + +.glyphicon-download:before { + content: "\e026"; +} + +.glyphicon-upload:before { + content: "\e027"; +} + +.glyphicon-inbox:before { + content: "\e028"; +} + +.glyphicon-play-circle:before { + content: "\e029"; +} + +.glyphicon-repeat:before { + content: "\e030"; +} + +.glyphicon-refresh:before { + content: "\e031"; +} + +.glyphicon-list-alt:before { + content: "\e032"; +} + +.glyphicon-flag:before { + content: "\e034"; +} + +.glyphicon-headphones:before { + content: "\e035"; +} + +.glyphicon-volume-off:before { + content: "\e036"; +} + +.glyphicon-volume-down:before { + content: "\e037"; +} + +.glyphicon-volume-up:before { + content: "\e038"; +} + +.glyphicon-qrcode:before { + content: "\e039"; +} + +.glyphicon-barcode:before { + content: "\e040"; +} + +.glyphicon-tag:before { + content: "\e041"; +} + +.glyphicon-tags:before { + content: "\e042"; +} + +.glyphicon-book:before { + content: "\e043"; +} + +.glyphicon-print:before { + content: "\e045"; +} + +.glyphicon-font:before { + content: "\e047"; +} + +.glyphicon-bold:before { + content: "\e048"; +} + +.glyphicon-italic:before { + content: "\e049"; +} + +.glyphicon-text-height:before { + content: "\e050"; +} + +.glyphicon-text-width:before { + content: "\e051"; +} + +.glyphicon-align-left:before { + content: "\e052"; +} + +.glyphicon-align-center:before { + content: "\e053"; +} + +.glyphicon-align-right:before { + content: "\e054"; +} + +.glyphicon-align-justify:before { + content: "\e055"; +} + +.glyphicon-list:before { + content: "\e056"; +} + +.glyphicon-indent-left:before { + content: "\e057"; +} + +.glyphicon-indent-right:before { + content: "\e058"; +} + +.glyphicon-facetime-video:before { + content: "\e059"; +} + +.glyphicon-picture:before { + content: "\e060"; +} + +.glyphicon-map-marker:before { + content: "\e062"; +} + +.glyphicon-adjust:before { + content: "\e063"; +} + +.glyphicon-tint:before { + content: "\e064"; +} + +.glyphicon-edit:before { + content: "\e065"; +} + +.glyphicon-share:before { + content: "\e066"; +} + +.glyphicon-check:before { + content: "\e067"; +} + +.glyphicon-move:before { + content: "\e068"; +} + +.glyphicon-step-backward:before { + content: "\e069"; +} + +.glyphicon-fast-backward:before { + content: "\e070"; +} + +.glyphicon-backward:before { + content: "\e071"; +} + +.glyphicon-play:before { + content: "\e072"; +} + +.glyphicon-pause:before { + content: "\e073"; +} + +.glyphicon-stop:before { + content: "\e074"; +} + +.glyphicon-forward:before { + content: "\e075"; +} + +.glyphicon-fast-forward:before { + content: "\e076"; +} + +.glyphicon-step-forward:before { + content: "\e077"; +} + +.glyphicon-eject:before { + content: "\e078"; +} + +.glyphicon-chevron-left:before { + content: "\e079"; +} + +.glyphicon-chevron-right:before { + content: "\e080"; +} + +.glyphicon-plus-sign:before { + content: "\e081"; +} + +.glyphicon-minus-sign:before { + content: "\e082"; +} + +.glyphicon-remove-sign:before { + content: "\e083"; +} + +.glyphicon-ok-sign:before { + content: "\e084"; +} + +.glyphicon-question-sign:before { + content: "\e085"; +} + +.glyphicon-info-sign:before { + content: "\e086"; +} + +.glyphicon-screenshot:before { + content: "\e087"; +} + +.glyphicon-remove-circle:before { + content: "\e088"; +} + +.glyphicon-ok-circle:before { + content: "\e089"; +} + +.glyphicon-ban-circle:before { + content: "\e090"; +} + +.glyphicon-arrow-left:before { + content: "\e091"; +} + +.glyphicon-arrow-right:before { + content: "\e092"; +} + +.glyphicon-arrow-up:before { + content: "\e093"; +} + +.glyphicon-arrow-down:before { + content: "\e094"; +} + +.glyphicon-share-alt:before { + content: "\e095"; +} + +.glyphicon-resize-full:before { + content: "\e096"; +} + +.glyphicon-resize-small:before { + content: "\e097"; +} + +.glyphicon-exclamation-sign:before { + content: "\e101"; +} + +.glyphicon-gift:before { + content: "\e102"; +} + +.glyphicon-leaf:before { + content: "\e103"; +} + +.glyphicon-eye-open:before { + content: "\e105"; +} + +.glyphicon-eye-close:before { + content: "\e106"; +} + +.glyphicon-warning-sign:before { + content: "\e107"; +} + +.glyphicon-plane:before { + content: "\e108"; +} + +.glyphicon-random:before { + content: "\e110"; +} + +.glyphicon-comment:before { + content: "\e111"; +} + +.glyphicon-magnet:before { + content: "\e112"; +} + +.glyphicon-chevron-up:before { + content: "\e113"; +} + +.glyphicon-chevron-down:before { + content: "\e114"; +} + +.glyphicon-retweet:before { + content: "\e115"; +} + +.glyphicon-shopping-cart:before { + content: "\e116"; +} + +.glyphicon-folder-close:before { + content: "\e117"; +} + +.glyphicon-folder-open:before { + content: "\e118"; +} + +.glyphicon-resize-vertical:before { + content: "\e119"; +} + +.glyphicon-resize-horizontal:before { + content: "\e120"; +} + +.glyphicon-hdd:before { + content: "\e121"; +} + +.glyphicon-bullhorn:before { + content: "\e122"; +} + +.glyphicon-certificate:before { + content: "\e124"; +} + +.glyphicon-thumbs-up:before { + content: "\e125"; +} + +.glyphicon-thumbs-down:before { + content: "\e126"; +} + +.glyphicon-hand-right:before { + content: "\e127"; +} + +.glyphicon-hand-left:before { + content: "\e128"; +} + +.glyphicon-hand-up:before { + content: "\e129"; +} + +.glyphicon-hand-down:before { + content: "\e130"; +} + +.glyphicon-circle-arrow-right:before { + content: "\e131"; +} + +.glyphicon-circle-arrow-left:before { + content: "\e132"; +} + +.glyphicon-circle-arrow-up:before { + content: "\e133"; +} + +.glyphicon-circle-arrow-down:before { + content: "\e134"; +} + +.glyphicon-globe:before { + content: "\e135"; +} + +.glyphicon-tasks:before { + content: "\e137"; +} + +.glyphicon-filter:before { + content: "\e138"; +} + +.glyphicon-fullscreen:before { + content: "\e140"; +} + +.glyphicon-dashboard:before { + content: "\e141"; +} + +.glyphicon-heart-empty:before { + content: "\e143"; +} + +.glyphicon-link:before { + content: "\e144"; +} + +.glyphicon-phone:before { + content: "\e145"; +} + +.glyphicon-usd:before { + content: "\e148"; +} + +.glyphicon-gbp:before { + content: "\e149"; +} + +.glyphicon-sort:before { + content: "\e150"; +} + +.glyphicon-sort-by-alphabet:before { + content: "\e151"; +} + +.glyphicon-sort-by-alphabet-alt:before { + content: "\e152"; +} + +.glyphicon-sort-by-order:before { + content: "\e153"; +} + +.glyphicon-sort-by-order-alt:before { + content: "\e154"; +} + +.glyphicon-sort-by-attributes:before { + content: "\e155"; +} + +.glyphicon-sort-by-attributes-alt:before { + content: "\e156"; +} + +.glyphicon-unchecked:before { + content: "\e157"; +} + +.glyphicon-expand:before { + content: "\e158"; +} + +.glyphicon-collapse-down:before { + content: "\e159"; +} + +.glyphicon-collapse-up:before { + content: "\e160"; +} + +.glyphicon-log-in:before { + content: "\e161"; +} + +.glyphicon-flash:before { + content: "\e162"; +} + +.glyphicon-log-out:before { + content: "\e163"; +} + +.glyphicon-new-window:before { + content: "\e164"; +} + +.glyphicon-record:before { + content: "\e165"; +} + +.glyphicon-save:before { + content: "\e166"; +} + +.glyphicon-open:before { + content: "\e167"; +} + +.glyphicon-saved:before { + content: "\e168"; +} + +.glyphicon-import:before { + content: "\e169"; +} + +.glyphicon-export:before { + content: "\e170"; +} + +.glyphicon-send:before { + content: "\e171"; +} + +.glyphicon-floppy-disk:before { + content: "\e172"; +} + +.glyphicon-floppy-saved:before { + content: "\e173"; +} + +.glyphicon-floppy-remove:before { + content: "\e174"; +} + +.glyphicon-floppy-save:before { + content: "\e175"; +} + +.glyphicon-floppy-open:before { + content: "\e176"; +} + +.glyphicon-credit-card:before { + content: "\e177"; +} + +.glyphicon-transfer:before { + content: "\e178"; +} + +.glyphicon-cutlery:before { + content: "\e179"; +} + +.glyphicon-header:before { + content: "\e180"; +} + +.glyphicon-compressed:before { + content: "\e181"; +} + +.glyphicon-earphone:before { + content: "\e182"; +} + +.glyphicon-phone-alt:before { + content: "\e183"; +} + +.glyphicon-tower:before { + content: "\e184"; +} + +.glyphicon-stats:before { + content: "\e185"; +} + +.glyphicon-sd-video:before { + content: "\e186"; +} + +.glyphicon-hd-video:before { + content: "\e187"; +} + +.glyphicon-subtitles:before { + content: "\e188"; +} + +.glyphicon-sound-stereo:before { + content: "\e189"; +} + +.glyphicon-sound-dolby:before { + content: "\e190"; +} + +.glyphicon-sound-5-1:before { + content: "\e191"; +} + +.glyphicon-sound-6-1:before { + content: "\e192"; +} + +.glyphicon-sound-7-1:before { + content: "\e193"; +} + +.glyphicon-copyright-mark:before { + content: "\e194"; +} + +.glyphicon-registration-mark:before { + content: "\e195"; +} + +.glyphicon-cloud-download:before { + content: "\e197"; +} + +.glyphicon-cloud-upload:before { + content: "\e198"; +} + +.glyphicon-tree-conifer:before { + content: "\e199"; +} + +.glyphicon-tree-deciduous:before { + content: "\e200"; +} + +.glyphicon-briefcase:before { + content: "\1f4bc"; +} + +.glyphicon-calendar:before { + content: "\1f4c5"; +} + +.glyphicon-pushpin:before { + content: "\1f4cc"; +} + +.glyphicon-paperclip:before { + content: "\1f4ce"; +} + +.glyphicon-camera:before { + content: "\1f4f7"; +} + +.glyphicon-lock:before { + content: "\1f512"; +} + +.glyphicon-bell:before { + content: "\1f514"; +} + +.glyphicon-bookmark:before { + content: "\1f516"; +} + +.glyphicon-fire:before { + content: "\1f525"; +} + +.glyphicon-wrench:before { + content: "\1f527"; +} + +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px solid #000000; + border-right: 4px solid transparent; + border-bottom: 0 dotted; + border-left: 4px solid transparent; + content: ""; +} + +.dropdown { + position: relative; +} + +.dropdown-toggle:focus { + outline: 0; +} + +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 14px; + list-style: none; + background-color: #ffffff; + border: 1px solid #cccccc; + border: 1px solid rgba(0, 0, 0, 0.15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + background-clip: padding-box; +} + +.dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.dropdown-menu .divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} + +.dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.428571429; + color: #333333; + white-space: nowrap; +} + +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + color: #ffffff; + text-decoration: none; + background-color: #428bca; +} + +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #ffffff; + text-decoration: none; + background-color: #428bca; + outline: 0; +} + +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + color: #999999; +} + +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + text-decoration: none; + cursor: not-allowed; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled=false); +} + +.open > .dropdown-menu { + display: block; +} + +.open > a { + outline: 0; +} + +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: 12px; + line-height: 1.428571429; + color: #999999; +} + +.dropdown-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 990; +} + +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} + +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + border-top: 0 dotted; + border-bottom: 4px solid #000000; + content: ""; +} + +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; +} + +@media (min-width: 768px) { + .navbar-right .dropdown-menu { + right: 0; + left: auto; + } +} + +.btn-default .caret { + border-top-color: #333333; +} + +.btn-primary .caret, +.btn-success .caret, +.btn-warning .caret, +.btn-danger .caret, +.btn-info .caret { + border-top-color: #fff; +} + +.dropup .btn-default .caret { + border-bottom-color: #333333; +} + +.dropup .btn-primary .caret, +.dropup .btn-success .caret, +.dropup .btn-warning .caret, +.dropup .btn-danger .caret, +.dropup .btn-info .caret { + border-bottom-color: #fff; +} + +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; +} + +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + float: left; +} + +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover, +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus, +.btn-group > .btn:active, +.btn-group-vertical > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn.active { + z-index: 2; +} + +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus { + outline: none; +} + +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group { + margin-left: -1px; +} + +.btn-toolbar:before, +.btn-toolbar:after { + display: table; + content: " "; +} + +.btn-toolbar:after { + clear: both; +} + +.btn-toolbar:before, +.btn-toolbar:after { + display: table; + content: " "; +} + +.btn-toolbar:after { + clear: both; +} + +.btn-toolbar .btn-group { + float: left; +} + +.btn-toolbar > .btn + .btn, +.btn-toolbar > .btn-group + .btn, +.btn-toolbar > .btn + .btn-group, +.btn-toolbar > .btn-group + .btn-group { + margin-left: 5px; +} + +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} + +.btn-group > .btn:first-child { + margin-left: 0; +} + +.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} + +.btn-group > .btn-group { + float: left; +} + +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} + +.btn-group > .btn-group:first-child > .btn:last-child, +.btn-group > .btn-group:first-child > .dropdown-toggle { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.btn-group > .btn-group:last-child > .btn:first-child { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} + +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} + +.btn-group-xs > .btn { + padding: 5px 10px; + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +.btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +.btn-group-lg > .btn { + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} + +.btn-group > .btn + .dropdown-toggle { + padding-right: 8px; + padding-left: 8px; +} + +.btn-group > .btn-lg + .dropdown-toggle { + padding-right: 12px; + padding-left: 12px; +} + +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); +} + +.btn .caret { + margin-left: 0; +} + +.btn-lg .caret { + border-width: 5px 5px 0; + border-bottom-width: 0; +} + +.dropup .btn-lg .caret { + border-width: 0 5px 5px; +} + +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group { + display: block; + float: none; + width: 100%; + max-width: 100%; +} + +.btn-group-vertical > .btn-group:before, +.btn-group-vertical > .btn-group:after { + display: table; + content: " "; +} + +.btn-group-vertical > .btn-group:after { + clear: both; +} + +.btn-group-vertical > .btn-group:before, +.btn-group-vertical > .btn-group:after { + display: table; + content: " "; +} + +.btn-group-vertical > .btn-group:after { + clear: both; +} + +.btn-group-vertical > .btn-group > .btn { + float: none; +} + +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} + +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; +} + +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-top-right-radius: 0; + border-bottom-left-radius: 4px; + border-top-left-radius: 0; +} + +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} + +.btn-group-vertical > .btn-group:first-child > .btn:last-child, +.btn-group-vertical > .btn-group:first-child > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.btn-group-vertical > .btn-group:last-child > .btn:first-child { + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.btn-group-justified { + display: table; + width: 100%; + border-collapse: separate; + table-layout: fixed; +} + +.btn-group-justified .btn { + display: table-cell; + float: none; + width: 1%; +} + +[data-toggle="buttons"] > .btn > input[type="radio"], +[data-toggle="buttons"] > .btn > input[type="checkbox"] { + display: none; +} + +.input-group { + position: relative; + display: table; + border-collapse: separate; +} + +.input-group.col { + float: none; + padding-right: 0; + padding-left: 0; +} + +.input-group .form-control { + width: 100%; + margin-bottom: 0; +} + +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + height: 45px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} + +select.input-group-lg > .form-control, +select.input-group-lg > .input-group-addon, +select.input-group-lg > .input-group-btn > .btn { + height: 45px; + line-height: 45px; +} + +textarea.input-group-lg > .form-control, +textarea.input-group-lg > .input-group-addon, +textarea.input-group-lg > .input-group-btn > .btn { + height: auto; +} + +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +select.input-group-sm > .form-control, +select.input-group-sm > .input-group-addon, +select.input-group-sm > .input-group-btn > .btn { + height: 30px; + line-height: 30px; +} + +textarea.input-group-sm > .form-control, +textarea.input-group-sm > .input-group-addon, +textarea.input-group-sm > .input-group-btn > .btn { + height: auto; +} + +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; +} + +.input-group-addon:not(:first-child):not(:last-child), +.input-group-btn:not(:first-child):not(:last-child), +.input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; +} + +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; +} + +.input-group-addon { + padding: 6px 12px; + font-size: 14px; + font-weight: normal; + line-height: 1; + text-align: center; + background-color: #eeeeee; + border: 1px solid #cccccc; + border-radius: 4px; +} + +.input-group-addon.input-sm { + padding: 5px 10px; + font-size: 12px; + border-radius: 3px; +} + +.input-group-addon.input-lg { + padding: 10px 16px; + font-size: 18px; + border-radius: 6px; +} + +.input-group-addon input[type="radio"], +.input-group-addon input[type="checkbox"] { + margin-top: 0; +} + +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} + +.input-group-addon:first-child { + border-right: 0; +} + +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child) { + border-bottom-left-radius: 0; + border-top-left-radius: 0; +} + +.input-group-addon:last-child { + border-left: 0; +} + +.input-group-btn { + position: relative; + white-space: nowrap; +} + +.input-group-btn > .btn { + position: relative; +} + +.input-group-btn > .btn + .btn { + margin-left: -4px; +} + +.input-group-btn > .btn:hover, +.input-group-btn > .btn:active { + z-index: 2; +} + +.nav { + padding-left: 0; + margin-bottom: 0; + list-style: none; +} + +.nav:before, +.nav:after { + display: table; + content: " "; +} + +.nav:after { + clear: both; +} + +.nav:before, +.nav:after { + display: table; + content: " "; +} + +.nav:after { + clear: both; +} + +.nav > li { + position: relative; + display: block; +} + +.nav > li > a { + position: relative; + display: block; + padding: 10px 15px; +} + +.nav > li > a:hover, +.nav > li > a:focus { + text-decoration: none; + background-color: #eeeeee; +} + +.nav > li.disabled > a { + color: #999999; +} + +.nav > li.disabled > a:hover, +.nav > li.disabled > a:focus { + color: #999999; + text-decoration: none; + cursor: not-allowed; + background-color: transparent; +} + +.nav .open > a, +.nav .open > a:hover, +.nav .open > a:focus { + background-color: #eeeeee; + border-color: #428bca; +} + +.nav .nav-divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} + +.nav > li > a > img { + max-width: none; +} + +.nav-tabs { + border-bottom: 1px solid #dddddd; +} + +.nav-tabs > li { + float: left; + margin-bottom: -1px; +} + +.nav-tabs > li > a { + margin-right: 2px; + line-height: 1.428571429; + border: 1px solid transparent; + border-radius: 4px 4px 0 0; +} + +.nav-tabs > li > a:hover { + border-color: #eeeeee #eeeeee #dddddd; +} + +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:hover, +.nav-tabs > li.active > a:focus { + color: #555555; + cursor: default; + background-color: #ffffff; + border: 1px solid #dddddd; + border-bottom-color: transparent; +} + +.nav-tabs.nav-justified { + width: 100%; + border-bottom: 0; +} + +.nav-tabs.nav-justified > li { + float: none; +} + +.nav-tabs.nav-justified > li > a { + text-align: center; +} + +@media (min-width: 768px) { + .nav-tabs.nav-justified > li { + display: table-cell; + width: 1%; + } +} + +.nav-tabs.nav-justified > li > a { + margin-right: 0; + border-bottom: 1px solid #dddddd; +} + +.nav-tabs.nav-justified > .active > a { + border-bottom-color: #ffffff; +} + +.nav-pills > li { + float: left; +} + +.nav-pills > li > a { + border-radius: 5px; +} + +.nav-pills > li + li { + margin-left: 2px; +} + +.nav-pills > li.active > a, +.nav-pills > li.active > a:hover, +.nav-pills > li.active > a:focus { + color: #ffffff; + background-color: #428bca; +} + +.nav-stacked > li { + float: none; +} + +.nav-stacked > li + li { + margin-top: 2px; + margin-left: 0; +} + +.nav-justified { + width: 100%; +} + +.nav-justified > li { + float: none; +} + +.nav-justified > li > a { + text-align: center; +} + +@media (min-width: 768px) { + .nav-justified > li { + display: table-cell; + width: 1%; + } +} + +.nav-tabs-justified { + border-bottom: 0; +} + +.nav-tabs-justified > li > a { + margin-right: 0; + border-bottom: 1px solid #dddddd; +} + +.nav-tabs-justified > .active > a { + border-bottom-color: #ffffff; +} + +.tabbable:before, +.tabbable:after { + display: table; + content: " "; +} + +.tabbable:after { + clear: both; +} + +.tabbable:before, +.tabbable:after { + display: table; + content: " "; +} + +.tabbable:after { + clear: both; +} + +.tab-content > .tab-pane, +.pill-content > .pill-pane { + display: none; +} + +.tab-content > .active, +.pill-content > .active { + display: block; +} + +.nav .caret { + border-top-color: #428bca; + border-bottom-color: #428bca; +} + +.nav a:hover .caret { + border-top-color: #2a6496; + border-bottom-color: #2a6496; +} + +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.navbar { + position: relative; + z-index: 1000; + min-height: 50px; + margin-bottom: 20px; + border: 1px solid transparent; +} + +.navbar:before, +.navbar:after { + display: table; + content: " "; +} + +.navbar:after { + clear: both; +} + +.navbar:before, +.navbar:after { + display: table; + content: " "; +} + +.navbar:after { + clear: both; +} + +@media (min-width: 768px) { + .navbar { + border-radius: 4px; + } +} + +.navbar-header:before, +.navbar-header:after { + display: table; + content: " "; +} + +.navbar-header:after { + clear: both; +} + +.navbar-header:before, +.navbar-header:after { + display: table; + content: " "; +} + +.navbar-header:after { + clear: both; +} + +@media (min-width: 768px) { + .navbar-header { + float: left; + } +} + +.navbar-collapse { + max-height: 340px; + padding-right: 15px; + padding-left: 15px; + overflow-x: visible; + border-top: 1px solid transparent; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); + -webkit-overflow-scrolling: touch; +} + +.navbar-collapse:before, +.navbar-collapse:after { + display: table; + content: " "; +} + +.navbar-collapse:after { + clear: both; +} + +.navbar-collapse:before, +.navbar-collapse:after { + display: table; + content: " "; +} + +.navbar-collapse:after { + clear: both; +} + +.navbar-collapse.in { + overflow-y: auto; +} + +@media (min-width: 768px) { + .navbar-collapse { + width: auto; + border-top: 0; + box-shadow: none; + } + .navbar-collapse.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; + overflow: visible !important; + } + .navbar-collapse.in { + overflow-y: visible; + } + .navbar-collapse .navbar-nav.navbar-left:first-child { + margin-left: -15px; + } + .navbar-collapse .navbar-nav.navbar-right:last-child { + margin-right: -15px; + } + .navbar-collapse .navbar-text:last-child { + margin-right: 0; + } +} + +.container > .navbar-header, +.container > .navbar-collapse { + margin-right: -15px; + margin-left: -15px; +} + +@media (min-width: 768px) { + .container > .navbar-header, + .container > .navbar-collapse { + margin-right: 0; + margin-left: 0; + } +} + +.navbar-static-top { + border-width: 0 0 1px; +} + +@media (min-width: 768px) { + .navbar-static-top { + border-radius: 0; + } +} + +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + border-width: 0 0 1px; +} + +@media (min-width: 768px) { + .navbar-fixed-top, + .navbar-fixed-bottom { + border-radius: 0; + } +} + +.navbar-fixed-top { + top: 0; + z-index: 1030; +} + +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; +} + +.navbar-brand { + float: left; + padding: 15px 15px; + font-size: 18px; + line-height: 20px; +} + +.navbar-brand:hover, +.navbar-brand:focus { + text-decoration: none; +} + +@media (min-width: 768px) { + .navbar > .container .navbar-brand { + margin-left: -15px; + } +} + +.navbar-toggle { + position: relative; + float: right; + padding: 9px 10px; + margin-top: 8px; + margin-right: 15px; + margin-bottom: 8px; + background-color: transparent; + border: 1px solid transparent; + border-radius: 4px; +} + +.navbar-toggle .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; +} + +.navbar-toggle .icon-bar + .icon-bar { + margin-top: 4px; +} + +@media (min-width: 768px) { + .navbar-toggle { + display: none; + } +} + +.navbar-nav { + margin: 7.5px -15px; +} + +.navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: 20px; +} + +@media (max-width: 767px) { + .navbar-nav .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + box-shadow: none; + } + .navbar-nav .open .dropdown-menu > li > a, + .navbar-nav .open .dropdown-menu .dropdown-header { + padding: 5px 15px 5px 25px; + } + .navbar-nav .open .dropdown-menu > li > a { + line-height: 20px; + } + .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-nav .open .dropdown-menu > li > a:focus { + background-image: none; + } +} + +@media (min-width: 768px) { + .navbar-nav { + float: left; + margin: 0; + } + .navbar-nav > li { + float: left; + } + .navbar-nav > li > a { + padding-top: 15px; + padding-bottom: 15px; + } +} + +@media (min-width: 768px) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + } +} + +.navbar-form { + padding: 10px 15px; + margin-top: 8px; + margin-right: -15px; + margin-bottom: 8px; + margin-left: -15px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1); +} + +@media (min-width: 768px) { + .navbar-form .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .form-control { + display: inline-block; + } + .navbar-form .radio, + .navbar-form .checkbox { + display: inline-block; + padding-left: 0; + margin-top: 0; + margin-bottom: 0; + } + .navbar-form .radio input[type="radio"], + .navbar-form .checkbox input[type="checkbox"] { + float: none; + margin-left: 0; + } +} + +@media (max-width: 767px) { + .navbar-form .form-group { + margin-bottom: 5px; + } +} + +@media (min-width: 768px) { + .navbar-form { + width: auto; + padding-top: 0; + padding-bottom: 0; + margin-right: 0; + margin-left: 0; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } +} + +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} + +.navbar-nav.pull-right > li > .dropdown-menu, +.navbar-nav > li > .dropdown-menu.pull-right { + right: 0; + left: auto; +} + +.navbar-btn { + margin-top: 8px; + margin-bottom: 8px; +} + +.navbar-text { + float: left; + margin-top: 15px; + margin-bottom: 15px; +} + +@media (min-width: 768px) { + .navbar-text { + margin-right: 15px; + margin-left: 15px; + } +} + +.navbar-default { + background-color: #f8f8f8; + border-color: #e7e7e7; +} + +.navbar-default .navbar-brand { + color: #777777; +} + +.navbar-default .navbar-brand:hover, +.navbar-default .navbar-brand:focus { + color: #5e5e5e; + background-color: transparent; +} + +.navbar-default .navbar-text { + color: #777777; +} + +.navbar-default .navbar-nav > li > a { + color: #777777; +} + +.navbar-default .navbar-nav > li > a:hover, +.navbar-default .navbar-nav > li > a:focus { + color: #333333; + background-color: transparent; +} + +.navbar-default .navbar-nav > .active > a, +.navbar-default .navbar-nav > .active > a:hover, +.navbar-default .navbar-nav > .active > a:focus { + color: #555555; + background-color: #e7e7e7; +} + +.navbar-default .navbar-nav > .disabled > a, +.navbar-default .navbar-nav > .disabled > a:hover, +.navbar-default .navbar-nav > .disabled > a:focus { + color: #cccccc; + background-color: transparent; +} + +.navbar-default .navbar-toggle { + border-color: #dddddd; +} + +.navbar-default .navbar-toggle:hover, +.navbar-default .navbar-toggle:focus { + background-color: #dddddd; +} + +.navbar-default .navbar-toggle .icon-bar { + background-color: #cccccc; +} + +.navbar-default .navbar-collapse, +.navbar-default .navbar-form { + border-color: #e6e6e6; +} + +.navbar-default .navbar-nav > .dropdown > a:hover .caret, +.navbar-default .navbar-nav > .dropdown > a:focus .caret { + border-top-color: #333333; + border-bottom-color: #333333; +} + +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .open > a:hover, +.navbar-default .navbar-nav > .open > a:focus { + color: #555555; + background-color: #e7e7e7; +} + +.navbar-default .navbar-nav > .open > a .caret, +.navbar-default .navbar-nav > .open > a:hover .caret, +.navbar-default .navbar-nav > .open > a:focus .caret { + border-top-color: #555555; + border-bottom-color: #555555; +} + +.navbar-default .navbar-nav > .dropdown > a .caret { + border-top-color: #777777; + border-bottom-color: #777777; +} + +@media (max-width: 767px) { + .navbar-default .navbar-nav .open .dropdown-menu > li > a { + color: #777777; + } + .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { + color: #333333; + background-color: transparent; + } + .navbar-default .navbar-nav .open .dropdown-menu > .active > a, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #555555; + background-color: #e7e7e7; + } + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #cccccc; + background-color: transparent; + } +} + +.navbar-default .navbar-link { + color: #777777; +} + +.navbar-default .navbar-link:hover { + color: #333333; +} + +.navbar-inverse { + background-color: #222222; + border-color: #080808; +} + +.navbar-inverse .navbar-brand { + color: #999999; +} + +.navbar-inverse .navbar-brand:hover, +.navbar-inverse .navbar-brand:focus { + color: #ffffff; + background-color: transparent; +} + +.navbar-inverse .navbar-text { + color: #999999; +} + +.navbar-inverse .navbar-nav > li > a { + color: #999999; +} + +.navbar-inverse .navbar-nav > li > a:hover, +.navbar-inverse .navbar-nav > li > a:focus { + color: #ffffff; + background-color: transparent; +} + +.navbar-inverse .navbar-nav > .active > a, +.navbar-inverse .navbar-nav > .active > a:hover, +.navbar-inverse .navbar-nav > .active > a:focus { + color: #ffffff; + background-color: #080808; +} + +.navbar-inverse .navbar-nav > .disabled > a, +.navbar-inverse .navbar-nav > .disabled > a:hover, +.navbar-inverse .navbar-nav > .disabled > a:focus { + color: #444444; + background-color: transparent; +} + +.navbar-inverse .navbar-toggle { + border-color: #333333; +} + +.navbar-inverse .navbar-toggle:hover, +.navbar-inverse .navbar-toggle:focus { + background-color: #333333; +} + +.navbar-inverse .navbar-toggle .icon-bar { + background-color: #ffffff; +} + +.navbar-inverse .navbar-collapse, +.navbar-inverse .navbar-form { + border-color: #101010; +} + +.navbar-inverse .navbar-nav > .open > a, +.navbar-inverse .navbar-nav > .open > a:hover, +.navbar-inverse .navbar-nav > .open > a:focus { + color: #ffffff; + background-color: #080808; +} + +.navbar-inverse .navbar-nav > .dropdown > a:hover .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +.navbar-inverse .navbar-nav > .dropdown > a .caret { + border-top-color: #999999; + border-bottom-color: #999999; +} + +.navbar-inverse .navbar-nav > .open > a .caret, +.navbar-inverse .navbar-nav > .open > a:hover .caret, +.navbar-inverse .navbar-nav > .open > a:focus .caret { + border-top-color: #ffffff; + border-bottom-color: #ffffff; +} + +@media (max-width: 767px) { + .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { + border-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { + color: #999999; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { + color: #ffffff; + background-color: transparent; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #ffffff; + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #444444; + background-color: transparent; + } +} + +.navbar-inverse .navbar-link { + color: #999999; +} + +.navbar-inverse .navbar-link:hover { + color: #ffffff; +} + +.breadcrumb { + padding: 8px 15px; + margin-bottom: 20px; + list-style: none; + background-color: #f5f5f5; + border-radius: 4px; +} + +.breadcrumb > li { + display: inline-block; +} + +.breadcrumb > li + li:before { + padding: 0 5px; + color: #cccccc; + content: "/\00a0"; +} + +.breadcrumb > .active { + color: #999999; +} + +.pagination { + display: inline-block; + padding-left: 0; + margin: 20px 0; + border-radius: 4px; +} + +.pagination > li { + display: inline; +} + +.pagination > li > a, +.pagination > li > span { + position: relative; + float: left; + padding: 6px 12px; + margin-left: -1px; + line-height: 1.428571429; + text-decoration: none; + background-color: #ffffff; + border: 1px solid #dddddd; +} + +.pagination > li:first-child > a, +.pagination > li:first-child > span { + margin-left: 0; + border-bottom-left-radius: 4px; + border-top-left-radius: 4px; +} + +.pagination > li:last-child > a, +.pagination > li:last-child > span { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +.pagination > li > a:hover, +.pagination > li > span:hover, +.pagination > li > a:focus, +.pagination > li > span:focus { + background-color: #eeeeee; +} + +.pagination > .active > a, +.pagination > .active > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + z-index: 2; + color: #ffffff; + cursor: default; + background-color: #428bca; + border-color: #428bca; +} + +.pagination > .disabled > span, +.pagination > .disabled > a, +.pagination > .disabled > a:hover, +.pagination > .disabled > a:focus { + color: #999999; + cursor: not-allowed; + background-color: #ffffff; + border-color: #dddddd; +} + +.pagination-lg > li > a, +.pagination-lg > li > span { + padding: 10px 16px; + font-size: 18px; +} + +.pagination-lg > li:first-child > a, +.pagination-lg > li:first-child > span { + border-bottom-left-radius: 6px; + border-top-left-radius: 6px; +} + +.pagination-lg > li:last-child > a, +.pagination-lg > li:last-child > span { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} + +.pagination-sm > li > a, +.pagination-sm > li > span { + padding: 5px 10px; + font-size: 12px; +} + +.pagination-sm > li:first-child > a, +.pagination-sm > li:first-child > span { + border-bottom-left-radius: 3px; + border-top-left-radius: 3px; +} + +.pagination-sm > li:last-child > a, +.pagination-sm > li:last-child > span { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} + +.pager { + padding-left: 0; + margin: 20px 0; + text-align: center; + list-style: none; +} + +.pager:before, +.pager:after { + display: table; + content: " "; +} + +.pager:after { + clear: both; +} + +.pager:before, +.pager:after { + display: table; + content: " "; +} + +.pager:after { + clear: both; +} + +.pager li { + display: inline; +} + +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #ffffff; + border: 1px solid #dddddd; + border-radius: 15px; +} + +.pager li > a:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #eeeeee; +} + +.pager .next > a, +.pager .next > span { + float: right; +} + +.pager .previous > a, +.pager .previous > span { + float: left; +} + +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #999999; + cursor: not-allowed; + background-color: #ffffff; +} + +.label { + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: #ffffff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} + +.label[href]:hover, +.label[href]:focus { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} + +.label:empty { + display: none; +} + +.label-default { + background-color: #999999; +} + +.label-default[href]:hover, +.label-default[href]:focus { + background-color: #808080; +} + +.label-primary { + background-color: #428bca; +} + +.label-primary[href]:hover, +.label-primary[href]:focus { + background-color: #3071a9; +} + +.label-success { + background-color: #5cb85c; +} + +.label-success[href]:hover, +.label-success[href]:focus { + background-color: #449d44; +} + +.label-info { + background-color: #5bc0de; +} + +.label-info[href]:hover, +.label-info[href]:focus { + background-color: #31b0d5; +} + +.label-warning { + background-color: #f0ad4e; +} + +.label-warning[href]:hover, +.label-warning[href]:focus { + background-color: #ec971f; +} + +.label-danger { + background-color: #d9534f; +} + +.label-danger[href]:hover, +.label-danger[href]:focus { + background-color: #c9302c; +} + +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: bold; + line-height: 1; + color: #ffffff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + background-color: #999999; + border-radius: 10px; +} + +.badge:empty { + display: none; +} + +a.badge:hover, +a.badge:focus { + color: #ffffff; + text-decoration: none; + cursor: pointer; +} + +.btn .badge { + position: relative; + top: -1px; +} + +a.list-group-item.active > .badge, +.nav-pills > .active > a > .badge { + color: #428bca; + background-color: #ffffff; +} + +.nav-pills > li > a > .badge { + margin-left: 3px; +} + +.jumbotron { + padding: 30px; + margin-bottom: 30px; + font-size: 21px; + font-weight: 200; + line-height: 2.1428571435; + color: inherit; + background-color: #eeeeee; +} + +.jumbotron h1 { + line-height: 1; + color: inherit; +} + +.jumbotron p { + line-height: 1.4; +} + +.container .jumbotron { + border-radius: 6px; +} + +@media screen and (min-width: 768px) { + .jumbotron { + padding-top: 48px; + padding-bottom: 48px; + } + .container .jumbotron { + padding-right: 60px; + padding-left: 60px; + } + .jumbotron h1 { + font-size: 63px; + } +} + +.thumbnail { + display: inline-block; + display: block; + height: auto; + max-width: 100%; + padding: 4px; + line-height: 1.428571429; + background-color: #ffffff; + border: 1px solid #dddddd; + border-radius: 4px; + -webkit-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} + +.thumbnail > img { + display: block; + height: auto; + max-width: 100%; +} + +a.thumbnail:hover, +a.thumbnail:focus { + border-color: #428bca; +} + +.thumbnail > img { + margin-right: auto; + margin-left: auto; +} + +.thumbnail .caption { + padding: 9px; + color: #333333; +} + +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} + +.alert h4 { + margin-top: 0; + color: inherit; +} + +.alert .alert-link { + font-weight: bold; +} + +.alert > p, +.alert > ul { + margin-bottom: 0; +} + +.alert > p + p { + margin-top: 5px; +} + +.alert-dismissable { + padding-right: 35px; +} + +.alert-dismissable .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} + +.alert-success { + color: #468847; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.alert-success hr { + border-top-color: #c9e2b3; +} + +.alert-success .alert-link { + color: #356635; +} + +.alert-info { + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.alert-info hr { + border-top-color: #a6e1ec; +} + +.alert-info .alert-link { + color: #2d6987; +} + +.alert-warning { + color: #c09853; + background-color: #fcf8e3; + border-color: #fbeed5; +} + +.alert-warning hr { + border-top-color: #f8e5be; +} + +.alert-warning .alert-link { + color: #a47e3c; +} + +.alert-danger { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; +} + +.alert-danger hr { + border-top-color: #e6c1c7; +} + +.alert-danger .alert-link { + color: #953b39; +} + +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-moz-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +@-o-keyframes progress-bar-stripes { + from { + background-position: 0 0; + } + to { + background-position: 40px 0; + } +} + +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} + +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f5f5f5; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.progress-bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + color: #ffffff; + text-align: center; + background-color: #428bca; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15); + -webkit-transition: width 0.6s ease; + transition: width 0.6s ease; +} + +.progress-striped .progress-bar { + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-size: 40px 40px; +} + +.progress.active .progress-bar { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + -ms-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} + +.progress-bar-success { + background-color: #5cb85c; +} + +.progress-striped .progress-bar-success { + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-bar-info { + background-color: #5bc0de; +} + +.progress-striped .progress-bar-info { + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-bar-warning { + background-color: #f0ad4e; +} + +.progress-striped .progress-bar-warning { + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.progress-bar-danger { + background-color: #d9534f; +} + +.progress-striped .progress-bar-danger { + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(255, 255, 255, 0.15)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(255, 255, 255, 0.15)), color-stop(0.75, rgba(255, 255, 255, 0.15)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); +} + +.media, +.media-body { + overflow: hidden; + zoom: 1; +} + +.media, +.media .media { + margin-top: 15px; +} + +.media:first-child { + margin-top: 0; +} + +.media-object { + display: block; +} + +.media-heading { + margin: 0 0 5px; +} + +.media > .pull-left { + margin-right: 10px; +} + +.media > .pull-right { + margin-left: 10px; +} + +.media-list { + padding-left: 0; + list-style: none; +} + +.list-group { + padding-left: 0; + margin-bottom: 20px; +} + +.list-group-item { + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #ffffff; + border: 1px solid #dddddd; +} + +.list-group-item:first-child { + border-top-right-radius: 4px; + border-top-left-radius: 4px; +} + +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} + +.list-group-item > .badge { + float: right; +} + +.list-group-item > .badge + .badge { + margin-right: 5px; +} + +a.list-group-item { + color: #555555; +} + +a.list-group-item .list-group-item-heading { + color: #333333; +} + +a.list-group-item:hover, +a.list-group-item:focus { + text-decoration: none; + background-color: #f5f5f5; +} + +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + z-index: 2; + color: #ffffff; + background-color: #428bca; + border-color: #428bca; +} + +.list-group-item.active .list-group-item-heading, +.list-group-item.active:hover .list-group-item-heading, +.list-group-item.active:focus .list-group-item-heading { + color: inherit; +} + +.list-group-item.active .list-group-item-text, +.list-group-item.active:hover .list-group-item-text, +.list-group-item.active:focus .list-group-item-text { + color: #e1edf7; +} + +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} + +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} + +.panel { + margin-bottom: 20px; + background-color: #ffffff; + border: 1px solid transparent; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.panel-body { + padding: 15px; +} + +.panel-body:before, +.panel-body:after { + display: table; + content: " "; +} + +.panel-body:after { + clear: both; +} + +.panel-body:before, +.panel-body:after { + display: table; + content: " "; +} + +.panel-body:after { + clear: both; +} + +.panel > .list-group { + margin-bottom: 0; +} + +.panel > .list-group .list-group-item { + border-width: 1px 0; +} + +.panel > .list-group .list-group-item:first-child { + border-top-right-radius: 0; + border-top-left-radius: 0; +} + +.panel > .list-group .list-group-item:last-child { + border-bottom: 0; +} + +.panel-heading + .list-group .list-group-item:first-child { + border-top-width: 0; +} + +.panel > .table { + margin-bottom: 0; +} + +.panel > .panel-body + .table { + border-top: 1px solid #dddddd; +} + +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + border-top-right-radius: 3px; + border-top-left-radius: 3px; +} + +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: 16px; +} + +.panel-title > a { + color: inherit; +} + +.panel-footer { + padding: 10px 15px; + background-color: #f5f5f5; + border-top: 1px solid #dddddd; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} + +.panel-group .panel { + margin-bottom: 0; + overflow: hidden; + border-radius: 4px; +} + +.panel-group .panel + .panel { + margin-top: 5px; +} + +.panel-group .panel-heading { + border-bottom: 0; +} + +.panel-group .panel-heading + .panel-collapse .panel-body { + border-top: 1px solid #dddddd; +} + +.panel-group .panel-footer { + border-top: 0; +} + +.panel-group .panel-footer + .panel-collapse .panel-body { + border-bottom: 1px solid #dddddd; +} + +.panel-default { + border-color: #dddddd; +} + +.panel-default > .panel-heading { + color: #333333; + background-color: #f5f5f5; + border-color: #dddddd; +} + +.panel-default > .panel-heading + .panel-collapse .panel-body { + border-top-color: #dddddd; +} + +.panel-default > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #dddddd; +} + +.panel-primary { + border-color: #428bca; +} + +.panel-primary > .panel-heading { + color: #ffffff; + background-color: #428bca; + border-color: #428bca; +} + +.panel-primary > .panel-heading + .panel-collapse .panel-body { + border-top-color: #428bca; +} + +.panel-primary > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #428bca; +} + +.panel-success { + border-color: #d6e9c6; +} + +.panel-success > .panel-heading { + color: #468847; + background-color: #dff0d8; + border-color: #d6e9c6; +} + +.panel-success > .panel-heading + .panel-collapse .panel-body { + border-top-color: #d6e9c6; +} + +.panel-success > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #d6e9c6; +} + +.panel-warning { + border-color: #fbeed5; +} + +.panel-warning > .panel-heading { + color: #c09853; + background-color: #fcf8e3; + border-color: #fbeed5; +} + +.panel-warning > .panel-heading + .panel-collapse .panel-body { + border-top-color: #fbeed5; +} + +.panel-warning > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #fbeed5; +} + +.panel-danger { + border-color: #eed3d7; +} + +.panel-danger > .panel-heading { + color: #b94a48; + background-color: #f2dede; + border-color: #eed3d7; +} + +.panel-danger > .panel-heading + .panel-collapse .panel-body { + border-top-color: #eed3d7; +} + +.panel-danger > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #eed3d7; +} + +.panel-info { + border-color: #bce8f1; +} + +.panel-info > .panel-heading { + color: #3a87ad; + background-color: #d9edf7; + border-color: #bce8f1; +} + +.panel-info > .panel-heading + .panel-collapse .panel-body { + border-top-color: #bce8f1; +} + +.panel-info > .panel-footer + .panel-collapse .panel-body { + border-bottom-color: #bce8f1; +} + +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); +} + +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, 0.15); +} + +.well-lg { + padding: 24px; + border-radius: 6px; +} + +.well-sm { + padding: 9px; + border-radius: 3px; +} + +.close { + float: right; + font-size: 21px; + font-weight: bold; + line-height: 1; + color: #000000; + text-shadow: 0 1px 0 #ffffff; + opacity: 0.2; + filter: alpha(opacity=20); +} + +.close:hover, +.close:focus { + color: #000000; + text-decoration: none; + cursor: pointer; + opacity: 0.5; + filter: alpha(opacity=50); +} + +button.close { + padding: 0; + cursor: pointer; + background: transparent; + border: 0; + -webkit-appearance: none; +} + +.modal-open { + overflow: hidden; +} + +body.modal-open, +.modal-open .navbar-fixed-top, +.modal-open .navbar-fixed-bottom { + margin-right: 15px; +} + +.modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + display: none; + overflow: auto; + overflow-y: scroll; +} + +.modal.fade .modal-dialog { + -webkit-transform: translate(0, -25%); + -ms-transform: translate(0, -25%); + transform: translate(0, -25%); + -webkit-transition: -webkit-transform 0.3s ease-out; + -moz-transition: -moz-transform 0.3s ease-out; + -o-transition: -o-transform 0.3s ease-out; + transition: transform 0.3s ease-out; +} + +.modal.in .modal-dialog { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); +} + +.modal-dialog { + z-index: 1050; + width: auto; + padding: 10px; + margin-right: auto; + margin-left: auto; +} + +.modal-content { + position: relative; + background-color: #ffffff; + border: 1px solid #999999; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + outline: none; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5); + background-clip: padding-box; +} + +.modal-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1030; + background-color: #000000; +} + +.modal-backdrop.fade { + opacity: 0; + filter: alpha(opacity=0); +} + +.modal-backdrop.in { + opacity: 0.5; + filter: alpha(opacity=50); +} + +.modal-header { + min-height: 16.428571429px; + padding: 15px; + border-bottom: 1px solid #e5e5e5; +} + +.modal-header .close { + margin-top: -2px; +} + +.modal-title { + margin: 0; + line-height: 1.428571429; +} + +.modal-body { + position: relative; + padding: 20px; +} + +.modal-footer { + padding: 19px 20px 20px; + margin-top: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} + +.modal-footer:before, +.modal-footer:after { + display: table; + content: " "; +} + +.modal-footer:after { + clear: both; +} + +.modal-footer:before, +.modal-footer:after { + display: table; + content: " "; +} + +.modal-footer:after { + clear: both; +} + +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} + +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} + +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} + +@media screen and (min-width: 768px) { + .modal-dialog { + right: auto; + left: 50%; + width: 600px; + padding-top: 30px; + padding-bottom: 30px; + } + .modal-content { + -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); + } +} + +.tooltip { + position: absolute; + z-index: 1030; + display: block; + font-size: 12px; + line-height: 1.4; + opacity: 0; + filter: alpha(opacity=0); + visibility: visible; +} + +.tooltip.in { + opacity: 0.9; + filter: alpha(opacity=90); +} + +.tooltip.top { + padding: 5px 0; + margin-top: -3px; +} + +.tooltip.right { + padding: 0 5px; + margin-left: 3px; +} + +.tooltip.bottom { + padding: 5px 0; + margin-top: 3px; +} + +.tooltip.left { + padding: 0 5px; + margin-left: -3px; +} + +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #ffffff; + text-align: center; + text-decoration: none; + background-color: #000000; + border-radius: 4px; +} + +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-top-color: #000000; + border-width: 5px 5px 0; +} + +.tooltip.top-left .tooltip-arrow { + bottom: 0; + left: 5px; + border-top-color: #000000; + border-width: 5px 5px 0; +} + +.tooltip.top-right .tooltip-arrow { + right: 5px; + bottom: 0; + border-top-color: #000000; + border-width: 5px 5px 0; +} + +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-right-color: #000000; + border-width: 5px 5px 5px 0; +} + +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-left-color: #000000; + border-width: 5px 0 5px 5px; +} + +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-bottom-color: #000000; + border-width: 0 5px 5px; +} + +.tooltip.bottom-left .tooltip-arrow { + top: 0; + left: 5px; + border-bottom-color: #000000; + border-width: 0 5px 5px; +} + +.tooltip.bottom-right .tooltip-arrow { + top: 0; + right: 5px; + border-bottom-color: #000000; + border-width: 0 5px 5px; +} + +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1010; + display: none; + max-width: 276px; + padding: 1px; + text-align: left; + white-space: normal; + background-color: #ffffff; + border: 1px solid #cccccc; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2); + background-clip: padding-box; +} + +.popover.top { + margin-top: -10px; +} + +.popover.right { + margin-left: 10px; +} + +.popover.bottom { + margin-top: 10px; +} + +.popover.left { + margin-left: -10px; +} + +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + font-weight: normal; + line-height: 18px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 5px 5px 0 0; +} + +.popover-content { + padding: 9px 14px; +} + +.popover .arrow, +.popover .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} + +.popover .arrow { + border-width: 11px; +} + +.popover .arrow:after { + border-width: 10px; + content: ""; +} + +.popover.top .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999999; + border-top-color: rgba(0, 0, 0, 0.25); + border-bottom-width: 0; +} + +.popover.top .arrow:after { + bottom: 1px; + margin-left: -10px; + border-top-color: #ffffff; + border-bottom-width: 0; + content: " "; +} + +.popover.right .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: #999999; + border-right-color: rgba(0, 0, 0, 0.25); + border-left-width: 0; +} + +.popover.right .arrow:after { + bottom: -10px; + left: 1px; + border-right-color: #ffffff; + border-left-width: 0; + content: " "; +} + +.popover.bottom .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-bottom-color: #999999; + border-bottom-color: rgba(0, 0, 0, 0.25); + border-top-width: 0; +} + +.popover.bottom .arrow:after { + top: 1px; + margin-left: -10px; + border-bottom-color: #ffffff; + border-top-width: 0; + content: " "; +} + +.popover.left .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-left-color: #999999; + border-left-color: rgba(0, 0, 0, 0.25); + border-right-width: 0; +} + +.popover.left .arrow:after { + right: 1px; + bottom: -10px; + border-left-color: #ffffff; + border-right-width: 0; + content: " "; +} + +.carousel { + position: relative; +} + +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} + +.carousel-inner > .item { + position: relative; + display: none; + -webkit-transition: 0.6s ease-in-out left; + transition: 0.6s ease-in-out left; +} + +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + display: block; + height: auto; + max-width: 100%; + line-height: 1; +} + +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} + +.carousel-inner > .active { + left: 0; +} + +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} + +.carousel-inner > .next { + left: 100%; +} + +.carousel-inner > .prev { + left: -100%; +} + +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} + +.carousel-inner > .active.left { + left: -100%; +} + +.carousel-inner > .active.right { + left: 100%; +} + +.carousel-control { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 15%; + font-size: 20px; + color: #ffffff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); + opacity: 0.5; + filter: alpha(opacity=50); +} + +.carousel-control.left { + background-image: -webkit-gradient(linear, 0 top, 100% top, from(rgba(0, 0, 0, 0.5)), to(rgba(0, 0, 0, 0.0001))); + background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, 0.5) 0), color-stop(rgba(0, 0, 0, 0.0001) 100%)); + background-image: -moz-linear-gradient(left, rgba(0, 0, 0, 0.5) 0, rgba(0, 0, 0, 0.0001) 100%); + background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0, rgba(0, 0, 0, 0.0001) 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); +} + +.carousel-control.right { + right: 0; + left: auto; + background-image: -webkit-gradient(linear, 0 top, 100% top, from(rgba(0, 0, 0, 0.0001)), to(rgba(0, 0, 0, 0.5))); + background-image: -webkit-linear-gradient(left, color-stop(rgba(0, 0, 0, 0.0001) 0), color-stop(rgba(0, 0, 0, 0.5) 100%)); + background-image: -moz-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0, rgba(0, 0, 0, 0.5) 100%); + background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0, rgba(0, 0, 0, 0.5) 100%); + background-repeat: repeat-x; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); +} + +.carousel-control:hover, +.carousel-control:focus { + color: #ffffff; + text-decoration: none; + opacity: 0.9; + filter: alpha(opacity=90); +} + +.carousel-control .icon-prev, +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-left, +.carousel-control .glyphicon-chevron-right { + position: absolute; + top: 50%; + left: 50%; + z-index: 5; + display: inline-block; +} + +.carousel-control .icon-prev, +.carousel-control .icon-next { + width: 20px; + height: 20px; + margin-top: -10px; + margin-left: -10px; + font-family: serif; +} + +.carousel-control .icon-prev:before { + content: '\2039'; +} + +.carousel-control .icon-next:before { + content: '\203a'; +} + +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + padding-left: 0; + margin-left: -30%; + text-align: center; + list-style: none; +} + +.carousel-indicators li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + cursor: pointer; + border: 1px solid #ffffff; + border-radius: 10px; +} + +.carousel-indicators .active { + width: 12px; + height: 12px; + margin: 0; + background-color: #ffffff; +} + +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #ffffff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); +} + +.carousel-caption .btn { + text-shadow: none; +} + +@media screen and (min-width: 768px) { + .carousel-control .icon-prev, + .carousel-control .icon-next { + width: 30px; + height: 30px; + margin-top: -15px; + margin-left: -15px; + font-size: 30px; + } + .carousel-caption { + right: 20%; + left: 20%; + padding-bottom: 30px; + } + .carousel-indicators { + bottom: 20px; + } +} + +.clearfix:before, +.clearfix:after { + display: table; + content: " "; +} + +.clearfix:after { + clear: both; +} + +.pull-right { + float: right !important; +} + +.pull-left { + float: left !important; +} + +.hide { + display: none !important; +} + +.show { + display: block !important; +} + +.invisible { + visibility: hidden; +} + +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} + +.affix { + position: fixed; +} + +@-ms-viewport { + width: device-width; +} + +@media screen and (max-width: 400px) { + @-ms-viewport { + width: 320px; + } +} + +.hidden { + display: none !important; + visibility: hidden !important; +} + +.visible-xs { + display: none !important; +} + +tr.visible-xs { + display: none !important; +} + +th.visible-xs, +td.visible-xs { + display: none !important; +} + +@media (max-width: 767px) { + .visible-xs { + display: block !important; + } + tr.visible-xs { + display: table-row !important; + } + th.visible-xs, + td.visible-xs { + display: table-cell !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .visible-xs.visible-sm { + display: block !important; + } + tr.visible-xs.visible-sm { + display: table-row !important; + } + th.visible-xs.visible-sm, + td.visible-xs.visible-sm { + display: table-cell !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .visible-xs.visible-md { + display: block !important; + } + tr.visible-xs.visible-md { + display: table-row !important; + } + th.visible-xs.visible-md, + td.visible-xs.visible-md { + display: table-cell !important; + } +} + +@media (min-width: 1200px) { + .visible-xs.visible-lg { + display: block !important; + } + tr.visible-xs.visible-lg { + display: table-row !important; + } + th.visible-xs.visible-lg, + td.visible-xs.visible-lg { + display: table-cell !important; + } +} + +.visible-sm { + display: none !important; +} + +tr.visible-sm { + display: none !important; +} + +th.visible-sm, +td.visible-sm { + display: none !important; +} + +@media (max-width: 767px) { + .visible-sm.visible-xs { + display: block !important; + } + tr.visible-sm.visible-xs { + display: table-row !important; + } + th.visible-sm.visible-xs, + td.visible-sm.visible-xs { + display: table-cell !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm { + display: block !important; + } + tr.visible-sm { + display: table-row !important; + } + th.visible-sm, + td.visible-sm { + display: table-cell !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .visible-sm.visible-md { + display: block !important; + } + tr.visible-sm.visible-md { + display: table-row !important; + } + th.visible-sm.visible-md, + td.visible-sm.visible-md { + display: table-cell !important; + } +} + +@media (min-width: 1200px) { + .visible-sm.visible-lg { + display: block !important; + } + tr.visible-sm.visible-lg { + display: table-row !important; + } + th.visible-sm.visible-lg, + td.visible-sm.visible-lg { + display: table-cell !important; + } +} + +.visible-md { + display: none !important; +} + +tr.visible-md { + display: none !important; +} + +th.visible-md, +td.visible-md { + display: none !important; +} + +@media (max-width: 767px) { + .visible-md.visible-xs { + display: block !important; + } + tr.visible-md.visible-xs { + display: table-row !important; + } + th.visible-md.visible-xs, + td.visible-md.visible-xs { + display: table-cell !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .visible-md.visible-sm { + display: block !important; + } + tr.visible-md.visible-sm { + display: table-row !important; + } + th.visible-md.visible-sm, + td.visible-md.visible-sm { + display: table-cell !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md { + display: block !important; + } + tr.visible-md { + display: table-row !important; + } + th.visible-md, + td.visible-md { + display: table-cell !important; + } +} + +@media (min-width: 1200px) { + .visible-md.visible-lg { + display: block !important; + } + tr.visible-md.visible-lg { + display: table-row !important; + } + th.visible-md.visible-lg, + td.visible-md.visible-lg { + display: table-cell !important; + } +} + +.visible-lg { + display: none !important; +} + +tr.visible-lg { + display: none !important; +} + +th.visible-lg, +td.visible-lg { + display: none !important; +} + +@media (max-width: 767px) { + .visible-lg.visible-xs { + display: block !important; + } + tr.visible-lg.visible-xs { + display: table-row !important; + } + th.visible-lg.visible-xs, + td.visible-lg.visible-xs { + display: table-cell !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .visible-lg.visible-sm { + display: block !important; + } + tr.visible-lg.visible-sm { + display: table-row !important; + } + th.visible-lg.visible-sm, + td.visible-lg.visible-sm { + display: table-cell !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .visible-lg.visible-md { + display: block !important; + } + tr.visible-lg.visible-md { + display: table-row !important; + } + th.visible-lg.visible-md, + td.visible-lg.visible-md { + display: table-cell !important; + } +} + +@media (min-width: 1200px) { + .visible-lg { + display: block !important; + } + tr.visible-lg { + display: table-row !important; + } + th.visible-lg, + td.visible-lg { + display: table-cell !important; + } +} + +.hidden-xs { + display: block !important; +} + +tr.hidden-xs { + display: table-row !important; +} + +th.hidden-xs, +td.hidden-xs { + display: table-cell !important; +} + +@media (max-width: 767px) { + .hidden-xs { + display: none !important; + } + tr.hidden-xs { + display: none !important; + } + th.hidden-xs, + td.hidden-xs { + display: none !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .hidden-xs.hidden-sm { + display: none !important; + } + tr.hidden-xs.hidden-sm { + display: none !important; + } + th.hidden-xs.hidden-sm, + td.hidden-xs.hidden-sm { + display: none !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-xs.hidden-md { + display: none !important; + } + tr.hidden-xs.hidden-md { + display: none !important; + } + th.hidden-xs.hidden-md, + td.hidden-xs.hidden-md { + display: none !important; + } +} + +@media (min-width: 1200px) { + .hidden-xs.hidden-lg { + display: none !important; + } + tr.hidden-xs.hidden-lg { + display: none !important; + } + th.hidden-xs.hidden-lg, + td.hidden-xs.hidden-lg { + display: none !important; + } +} + +.hidden-sm { + display: block !important; +} + +tr.hidden-sm { + display: table-row !important; +} + +th.hidden-sm, +td.hidden-sm { + display: table-cell !important; +} + +@media (max-width: 767px) { + .hidden-sm.hidden-xs { + display: none !important; + } + tr.hidden-sm.hidden-xs { + display: none !important; + } + th.hidden-sm.hidden-xs, + td.hidden-sm.hidden-xs { + display: none !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .hidden-sm { + display: none !important; + } + tr.hidden-sm { + display: none !important; + } + th.hidden-sm, + td.hidden-sm { + display: none !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-sm.hidden-md { + display: none !important; + } + tr.hidden-sm.hidden-md { + display: none !important; + } + th.hidden-sm.hidden-md, + td.hidden-sm.hidden-md { + display: none !important; + } +} + +@media (min-width: 1200px) { + .hidden-sm.hidden-lg { + display: none !important; + } + tr.hidden-sm.hidden-lg { + display: none !important; + } + th.hidden-sm.hidden-lg, + td.hidden-sm.hidden-lg { + display: none !important; + } +} + +.hidden-md { + display: block !important; +} + +tr.hidden-md { + display: table-row !important; +} + +th.hidden-md, +td.hidden-md { + display: table-cell !important; +} + +@media (max-width: 767px) { + .hidden-md.hidden-xs { + display: none !important; + } + tr.hidden-md.hidden-xs { + display: none !important; + } + th.hidden-md.hidden-xs, + td.hidden-md.hidden-xs { + display: none !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .hidden-md.hidden-sm { + display: none !important; + } + tr.hidden-md.hidden-sm { + display: none !important; + } + th.hidden-md.hidden-sm, + td.hidden-md.hidden-sm { + display: none !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-md { + display: none !important; + } + tr.hidden-md { + display: none !important; + } + th.hidden-md, + td.hidden-md { + display: none !important; + } +} + +@media (min-width: 1200px) { + .hidden-md.hidden-lg { + display: none !important; + } + tr.hidden-md.hidden-lg { + display: none !important; + } + th.hidden-md.hidden-lg, + td.hidden-md.hidden-lg { + display: none !important; + } +} + +.hidden-lg { + display: block !important; +} + +tr.hidden-lg { + display: table-row !important; +} + +th.hidden-lg, +td.hidden-lg { + display: table-cell !important; +} + +@media (max-width: 767px) { + .hidden-lg.hidden-xs { + display: none !important; + } + tr.hidden-lg.hidden-xs { + display: none !important; + } + th.hidden-lg.hidden-xs, + td.hidden-lg.hidden-xs { + display: none !important; + } +} + +@media (min-width: 768px) and (max-width: 991px) { + .hidden-lg.hidden-sm { + display: none !important; + } + tr.hidden-lg.hidden-sm { + display: none !important; + } + th.hidden-lg.hidden-sm, + td.hidden-lg.hidden-sm { + display: none !important; + } +} + +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-lg.hidden-md { + display: none !important; + } + tr.hidden-lg.hidden-md { + display: none !important; + } + th.hidden-lg.hidden-md, + td.hidden-lg.hidden-md { + display: none !important; + } +} + +@media (min-width: 1200px) { + .hidden-lg { + display: none !important; + } + tr.hidden-lg { + display: none !important; + } + th.hidden-lg, + td.hidden-lg { + display: none !important; + } +} + +.visible-print { + display: none !important; +} + +tr.visible-print { + display: none !important; +} + +th.visible-print, +td.visible-print { + display: none !important; +} + +@media print { + .visible-print { + display: block !important; + } + tr.visible-print { + display: table-row !important; + } + th.visible-print, + td.visible-print { + display: table-cell !important; + } + .hidden-print { + display: none !important; + } + tr.hidden-print { + display: none !important; + } + th.hidden-print, + td.hidden-print { + display: none !important; + } +} \ No newline at end of file