From 64bdbdc981cc8d3c295a0acf3e01b6a0948f5da5 Mon Sep 17 00:00:00 2001 From: Deon George Date: Sat, 21 Aug 2010 14:43:03 +1000 Subject: [PATCH] Added Kohana v3.0.8 --- .htaccess | 21 + application/bootstrap.php | 102 + application/classes/controller/welcome.php | 10 + includes/kohana/LICENSE.md | 14 + includes/kohana/README.md | 3 + includes/kohana/install.php | 209 ++ includes/kohana/modules/auth/classes/auth.php | 3 + .../kohana/modules/auth/classes/auth/file.php | 3 + .../kohana/modules/auth/classes/auth/orm.php | 3 + .../modules/auth/classes/kohana/auth.php | 241 ++ .../modules/auth/classes/kohana/auth/file.php | 88 + .../modules/auth/classes/kohana/auth/orm.php | 288 ++ .../modules/auth/classes/model/auth/role.php | 27 + .../modules/auth/classes/model/auth/user.php | 244 ++ .../auth/classes/model/auth/user/token.php | 101 + .../modules/auth/classes/model/role.php | 7 + .../modules/auth/classes/model/user.php | 7 + .../modules/auth/classes/model/user/token.php | 7 + includes/kohana/modules/auth/config/auth.php | 16 + includes/kohana/modules/auth/mysql.sql | 48 + includes/kohana/modules/auth/postgresql.sql | 53 + includes/kohana/modules/cache/README.md | 60 + .../kohana/modules/cache/classes/cache.php | 3 + .../modules/cache/classes/cache/apc.php | 3 + .../cache/classes/cache/eaccelerator.php | 3 + .../modules/cache/classes/cache/file.php | 3 + .../modules/cache/classes/cache/memcache.php | 3 + .../cache/classes/cache/memcachetag.php | 3 + .../modules/cache/classes/cache/sqlite.php | 3 + .../modules/cache/classes/cache/xcache.php | 3 + .../modules/cache/classes/kohana/cache.php | 256 ++ .../cache/classes/kohana/cache/apc.php | 133 + .../classes/kohana/cache/eaccelerator.php | 133 + .../cache/classes/kohana/cache/exception.php | 3 + .../cache/classes/kohana/cache/file.php | 445 +++ .../classes/kohana/cache/garbagecollect.php | 23 + .../cache/classes/kohana/cache/memcache.php | 311 ++ .../classes/kohana/cache/memcachetag.php | 76 + .../cache/classes/kohana/cache/sqlite.php | 336 ++ .../cache/classes/kohana/cache/tagging.php | 42 + .../cache/classes/kohana/cache/xcache.php | 84 + .../kohana/modules/cache/config/cache.php | 71 + .../kohana/modules/cache/guide/cache.about.md | 59 + .../modules/cache/guide/cache.config.md | 168 + .../kohana/modules/cache/guide/cache.usage.md | 219 ++ .../kohana/modules/cache/guide/menu.cache.md | 4 + .../cache/tests/cache/KohanaCacheTest.php | 91 + .../kohana/modules/cache/tests/phpunit.xml | 16 + .../codebench/classes/bench/arrcallback.php | 57 + .../classes/bench/autolinkemails.php | 70 + .../codebench/classes/bench/datespan.php | 186 ++ .../codebench/classes/bench/explodelimit.php | 34 + .../codebench/classes/bench/gruberurl.php | 61 + .../codebench/classes/bench/ltrimdigits.php | 28 + .../codebench/classes/bench/mddobaseurl.php | 66 + .../codebench/classes/bench/mddoimageurl.php | 66 + .../classes/bench/mddoincludeviews.php | 50 + .../classes/bench/stripnullbytes.php | 37 + .../codebench/classes/bench/transliterate.php | 65 + .../codebench/classes/bench/urlsite.php | 123 + .../codebench/classes/bench/userfuncarray.php | 58 + .../codebench/classes/bench/validcolor.php | 116 + .../codebench/classes/bench/validurl.php | 105 + .../modules/codebench/classes/codebench.php | 3 + .../classes/controller/codebench.php | 32 + .../codebench/classes/kohana/codebench.php | 217 ++ .../modules/codebench/config/codebench.php | 16 + includes/kohana/modules/codebench/init.php | 8 + .../modules/codebench/views/codebench.php | 258 ++ .../modules/database/classes/database.php | 3 + .../database/classes/database/exception.php | 3 + .../database/classes/database/expression.php | 3 + .../database/classes/database/mysql.php | 3 + .../classes/database/mysql/result.php | 3 + .../modules/database/classes/database/pdo.php | 3 + .../database/classes/database/query.php | 3 + .../classes/database/query/builder.php | 3 + .../classes/database/query/builder/delete.php | 3 + .../classes/database/query/builder/insert.php | 3 + .../classes/database/query/builder/join.php | 3 + .../classes/database/query/builder/select.php | 3 + .../classes/database/query/builder/update.php | 3 + .../classes/database/query/builder/where.php | 3 + .../database/classes/database/result.php | 3 + .../classes/database/result/cached.php | 3 + .../kohana/modules/database/classes/db.php | 3 + .../classes/kohana/config/database.php | 97 + .../database/classes/kohana/database.php | 588 ++++ .../classes/kohana/database/exception.php | 11 + .../classes/kohana/database/expression.php | 60 + .../classes/kohana/database/mysql.php | 381 +++ .../classes/kohana/database/mysql/result.php | 71 + .../database/classes/kohana/database/pdo.php | 186 ++ .../classes/kohana/database/query.php | 218 ++ .../classes/kohana/database/query/builder.php | 199 ++ .../kohana/database/query/builder/delete.php | 89 + .../kohana/database/query/builder/insert.php | 171 + .../kohana/database/query/builder/join.php | 107 + .../kohana/database/query/builder/select.php | 388 +++ .../kohana/database/query/builder/update.php | 124 + .../kohana/database/query/builder/where.php | 160 + .../classes/kohana/database/result.php | 322 ++ .../classes/kohana/database/result/cached.php | 51 + .../modules/database/classes/kohana/db.php | 126 + .../classes/kohana/session/database.php | 229 ++ .../database/classes/session/database.php | 3 + .../modules/database/config/database.php | 57 + .../modules/database/config/session.php | 27 + includes/kohana/modules/image/README.markdown | 0 .../kohana/modules/image/classes/image.php | 3 + .../kohana/modules/image/classes/image/gd.php | 3 + .../modules/image/classes/kohana/image.php | 742 +++++ .../modules/image/classes/kohana/image/gd.php | 582 ++++ includes/kohana/modules/oauth/README.md | 5 + .../modules/oauth/classes/kohana/oauth.php | 217 ++ .../oauth/classes/kohana/oauth/consumer.php | 99 + .../oauth/classes/kohana/oauth/exception.php | 12 + .../oauth/classes/kohana/oauth/provider.php | 215 ++ .../classes/kohana/oauth/provider/google.php | 55 + .../classes/kohana/oauth/provider/twitter.php | 39 + .../oauth/classes/kohana/oauth/request.php | 478 +++ .../classes/kohana/oauth/request/access.php | 32 + .../kohana/oauth/request/authorize.php | 26 + .../kohana/oauth/request/credentials.php | 16 + .../classes/kohana/oauth/request/resource.php | 27 + .../classes/kohana/oauth/request/token.php | 32 + .../oauth/classes/kohana/oauth/response.php | 51 + .../oauth/classes/kohana/oauth/server.php | 14 + .../oauth/classes/kohana/oauth/signature.php | 77 + .../kohana/oauth/signature/hmac/sha1.php | 68 + .../kohana/oauth/signature/plaintext.php | 57 + .../oauth/classes/kohana/oauth/token.php | 94 + .../classes/kohana/oauth/token/access.php | 16 + .../classes/kohana/oauth/token/request.php | 36 + .../kohana/modules/oauth/classes/oauth.php | 3 + .../modules/oauth/classes/oauth/consumer.php | 3 + .../modules/oauth/classes/oauth/provider.php | 3 + .../oauth/classes/oauth/provider/google.php | 3 + .../oauth/classes/oauth/provider/twitter.php | 3 + .../modules/oauth/classes/oauth/request.php | 3 + .../oauth/classes/oauth/request/access.php | 3 + .../oauth/classes/oauth/request/authorize.php | 3 + .../classes/oauth/request/credentials.php | 3 + .../oauth/classes/oauth/request/resource.php | 3 + .../oauth/classes/oauth/request/token.php | 3 + .../modules/oauth/classes/oauth/response.php | 3 + .../modules/oauth/classes/oauth/signature.php | 3 + .../classes/oauth/signature/hmac/sha1.php | 3 + .../classes/oauth/signature/plaintext.php | 3 + .../modules/oauth/classes/oauth/token.php | 3 + .../oauth/classes/oauth/token/access.php | 3 + .../oauth/classes/oauth/token/request.php | 3 + .../kohana/modules/oauth/config/oauth.php | 15 + .../kohana/modules/oauth/config/userguide.php | 23 + .../modules/oauth/guide/oauth/config.md | 24 + .../kohana/modules/oauth/guide/oauth/index.md | 14 + .../kohana/modules/oauth/guide/oauth/menu.md | 3 + .../kohana/modules/oauth/guide/oauth/usage.md | 3 + .../oauth/views/oauth/twitter/tweet.php | 15 + .../kohana/modules/orm/classes/kohana/orm.php | 1346 ++++++++ includes/kohana/modules/orm/classes/orm.php | 3 + .../pagination/classes/kohana/pagination.php | 285 ++ .../modules/pagination/classes/pagination.php | 3 + .../modules/pagination/config/pagination.php | 15 + .../pagination/views/pagination/basic.php | 37 + .../pagination/views/pagination/floating.php | 94 + .../kohana/modules/unittest/README.markdown | 70 + .../unittest/classes/controller/unittest.php | 359 ++ .../modules/unittest/classes/kohana/tests.php | 263 ++ .../classes/kohana/unittest/helpers.php | 176 + .../classes/kohana/unittest/runner.php | 304 ++ .../classes/kohana/unittest/testcase.php | 102 + .../modules/unittest/config/unittest.php | 53 + .../modules/unittest/example.phpunit.xml | 16 + .../modules/unittest/guide/menu.unittest.md | 5 + .../unittest/guide/unittest.mockobjects.md | 265 ++ .../unittest/guide/unittest.testing.md | 117 + .../guide/unittest.testing_workflows.md | 51 + .../guide/unittest.troubleshooting.md | 21 + includes/kohana/modules/unittest/init.php | 23 + includes/kohana/modules/unittest/tests.php | 20 + .../modules/unittest/views/unittest/index.php | 66 + .../unittest/views/unittest/layout.php | 255 ++ .../unittest/views/unittest/results.php | 83 + includes/kohana/modules/userguide/README.md | 16 + .../classes/controller/userguide.php | 314 ++ .../modules/userguide/classes/kodoc.php | 3 + .../modules/userguide/classes/kodoc/class.php | 3 + .../userguide/classes/kodoc/markdown.php | 3 + .../userguide/classes/kodoc/method.php | 3 + .../userguide/classes/kodoc/method/param.php | 3 + .../userguide/classes/kodoc/missing.php | 3 + .../userguide/classes/kodoc/property.php | 3 + .../userguide/classes/kohana/kodoc.php | 325 ++ .../userguide/classes/kohana/kodoc/class.php | 167 + .../classes/kohana/kodoc/markdown.php | 169 + .../userguide/classes/kohana/kodoc/method.php | 141 + .../classes/kohana/kodoc/method/param.php | 101 + .../classes/kohana/kodoc/missing.php | 36 + .../classes/kohana/kodoc/property.php | 67 + .../modules/userguide/config/userguide.php | 17 + .../userguide/guide/about.conventions.md | 293 ++ .../userguide/guide/about.filesystem.md | 86 + .../modules/userguide/guide/about.flow.md | 73 + .../modules/userguide/guide/about.install.md | 96 + .../modules/userguide/guide/about.kohana.md | 18 + .../modules/userguide/guide/about.mvc.md | 7 + .../userguide/guide/about.translation.md | 4 + .../userguide/guide/about.upgrading.md | 290 ++ .../guide/de-de/about.conventions.md | 300 ++ .../userguide/guide/de-de/about.kohana.md | 15 + .../modules/userguide/guide/de-de/menu.md | 31 + .../modules/userguide/guide/debugging.code.md | 18 + .../userguide/guide/debugging.errors.md | 22 + .../userguide/guide/debugging.profiling.md | 20 + .../userguide/guide/es-es/debugging.errors.md | 24 + .../userguide/guide/es-es/debugging.md | 24 + .../guide/es-es/debugging.profiling.md | 22 + .../modules/userguide/guide/es-es/features.md | 1 + .../modules/userguide/guide/es-es/menu.md | 23 + .../userguide/guide/es-es/security.cookies.md | 3 + .../guide/es-es/security.database.md | 3 + .../modules/userguide/guide/es-es/security.md | 3 + .../guide/es-es/security.validation.md | 3 + .../userguide/guide/es-es/security.xss.md | 15 + .../guide/es-es/start.autoloading.md | 17 + .../guide/es-es/start.configuration.md | 94 + .../guide/es-es/start.conventions.md | 26 + .../userguide/guide/es-es/start.filesystem.md | 13 + .../userguide/guide/es-es/start.flow.md | 21 + .../guide/es-es/start.installation.md | 19 + .../modules/userguide/guide/es-es/start.md | 11 + .../userguide/guide/es-es/start.mvc.md | 5 + .../guide/es-es/tutorials.databases.md | 3 + .../guide/es-es/tutorials.helloworld.md | 106 + .../userguide/guide/es-es/tutorials.md | 7 + .../userguide/guide/es-es/tutorials.urls.md | 3 + .../userguide/guide/es-es/upgrading.md | 5 + .../modules/userguide/guide/features.md | 1 + .../guide/fr-fr/about.autoloading.md | 17 + .../guide/fr-fr/about.configuration.md | 95 + .../guide/fr-fr/about.conventions.md | 26 + .../userguide/guide/fr-fr/about.filesystem.md | 13 + .../userguide/guide/fr-fr/about.flow.md | 21 + .../userguide/guide/fr-fr/about.install.md | 20 + .../userguide/guide/fr-fr/about.kohana.md | 11 + .../userguide/guide/fr-fr/about.mvc.md | 5 + .../userguide/guide/fr-fr/about.upgrading.md | 290 ++ .../userguide/guide/fr-fr/debugging.code.md | 24 + .../userguide/guide/fr-fr/debugging.errors.md | 24 + .../guide/fr-fr/debugging.profiling.md | 22 + .../modules/userguide/guide/fr-fr/features.md | 1 + .../modules/userguide/guide/fr-fr/menu.md | 26 + .../userguide/guide/fr-fr/security.cookies.md | 3 + .../guide/fr-fr/security.database.md | 3 + .../guide/fr-fr/security.validation.md | 241 ++ .../userguide/guide/fr-fr/security.xss.md | 17 + .../guide/fr-fr/tutorials.databases.md | 242 ++ .../userguide/guide/fr-fr/tutorials.git.md | 117 + .../guide/fr-fr/tutorials.helloworld.md | 103 + .../userguide/guide/fr-fr/tutorials.orm.md | 121 + .../userguide/guide/fr-fr/tutorials.urls.md | 163 + .../guide/he-il/about.autoloading.md | 18 + .../guide/he-il/about.configuration.md | 99 + .../userguide/guide/he-il/about.filesystem.md | 18 + .../userguide/guide/he-il/about.flow.md | 22 + .../userguide/guide/he-il/about.kohana.md | 21 + .../userguide/guide/he-il/about.mvc.md | 29 + .../userguide/guide/he-il/debugging.code.md | 24 + .../userguide/guide/he-il/debugging.errors.md | 29 + .../guide/he-il/debugging.profiling.md | 21 + .../modules/userguide/guide/he-il/menu.md | 26 + .../kohana/modules/userguide/guide/menu.md | 31 + .../userguide/guide/nl/about.conventions.md | 316 ++ .../userguide/guide/nl/about.filesystem.md | 76 + .../modules/userguide/guide/nl/about.flow.md | 73 + .../userguide/guide/nl/about.install.md | 94 + .../userguide/guide/nl/about.kohana.md | 15 + .../modules/userguide/guide/nl/about.mvc.md | 7 + .../userguide/guide/nl/about.translation.md | 4 + .../userguide/guide/nl/about.upgrading.md | 288 ++ .../userguide/guide/nl/debugging.code.md | 18 + .../userguide/guide/nl/debugging.errors.md | 22 + .../userguide/guide/nl/debugging.profiling.md | 20 + .../modules/userguide/guide/nl/features.md | 1 + .../kohana/modules/userguide/guide/nl/menu.md | 31 + .../userguide/guide/nl/security.cookies.md | 3 + .../userguide/guide/nl/security.database.md | 3 + .../userguide/guide/nl/security.validation.md | 244 ++ .../userguide/guide/nl/security.xss.md | 15 + .../userguide/guide/nl/tutorials.databases.md | 248 ++ .../userguide/guide/nl/tutorials.git.md | 149 + .../guide/nl/tutorials.helloworld.md | 106 + .../userguide/guide/nl/tutorials.orm.md | 298 ++ .../guide/nl/tutorials.removeindex.md | 88 + .../userguide/guide/nl/tutorials.urls.md | 159 + .../userguide/guide/nl/using.autoloading.md | 95 + .../userguide/guide/nl/using.configuration.md | 57 + .../userguide/guide/nl/using.messages.md | 26 + .../userguide/guide/nl/using.sessions.md | 223 ++ .../modules/userguide/guide/nl/using.views.md | 118 + .../guide/ru-ru/about.conventions.md | 301 ++ .../userguide/guide/ru-ru/about.filesystem.md | 59 + .../userguide/guide/ru-ru/about.flow.md | 73 + .../userguide/guide/ru-ru/about.install.md | 89 + .../userguide/guide/ru-ru/about.kohana.md | 15 + .../userguide/guide/ru-ru/about.mvc.md | 5 + .../userguide/guide/ru-ru/about.upgrading.md | 292 ++ .../userguide/guide/ru-ru/debugging.code.md | 18 + .../userguide/guide/ru-ru/debugging.errors.md | 22 + .../guide/ru-ru/debugging.profiling.md | 20 + .../modules/userguide/guide/ru-ru/features.md | 1 + .../modules/userguide/guide/ru-ru/menu.md | 32 + .../userguide/guide/ru-ru/security.cookies.md | 3 + .../guide/ru-ru/security.database.md | 3 + .../guide/ru-ru/security.validation.md | 243 ++ .../userguide/guide/ru-ru/security.xss.md | 15 + .../guide/ru-ru/start.autoloading.md | 17 + .../guide/ru-ru/start.configuration.md | 92 + .../guide/ru-ru/start.controllers.md | 104 + .../guide/ru-ru/start.conventions.md | 26 + .../userguide/guide/ru-ru/start.filesystem.md | 13 + .../userguide/guide/ru-ru/start.flow.md | 22 + .../guide/ru-ru/start.installation.md | 19 + .../modules/userguide/guide/ru-ru/start.md | 11 + .../userguide/guide/ru-ru/start.mvc.md | 5 + .../guide/ru-ru/tutorials.databases.md | 249 ++ .../userguide/guide/ru-ru/tutorials.git.md | 149 + .../guide/ru-ru/tutorials.helloworld.md | 105 + .../userguide/guide/ru-ru/tutorials.orm.md | 312 ++ .../guide/ru-ru/tutorials.removeindex.md | 88 + .../userguide/guide/ru-ru/tutorials.urls.md | 160 + .../guide/ru-ru/using.autoloading.md | 96 + .../guide/ru-ru/using.configuration.md | 58 + .../userguide/guide/ru-ru/using.messages.md | 26 + .../userguide/guide/ru-ru/using.sessions.md | 223 ++ .../guide/ru-ru/using.translation.md | 1 + .../userguide/guide/ru-ru/using.views.md | 118 + .../userguide/guide/security.cookies.md | 3 + .../userguide/guide/security.database.md | 3 + .../userguide/guide/security.validation.md | 245 ++ .../modules/userguide/guide/security.xss.md | 15 + .../userguide/guide/tutorials.databases.md | 248 ++ .../modules/userguide/guide/tutorials.git.md | 149 + .../userguide/guide/tutorials.helloworld.md | 106 + .../modules/userguide/guide/tutorials.orm.md | 298 ++ .../userguide/guide/tutorials.removeindex.md | 88 + .../modules/userguide/guide/tutorials.urls.md | 159 + .../userguide/guide/using.autoloading.md | 95 + .../userguide/guide/using.configuration.md | 57 + .../modules/userguide/guide/using.messages.md | 26 + .../modules/userguide/guide/using.sessions.md | 223 ++ .../modules/userguide/guide/using.views.md | 120 + .../guide/zh-cn/about.conventions.md | 303 ++ .../userguide/guide/zh-cn/about.filesystem.md | 75 + .../userguide/guide/zh-cn/about.flow.md | 73 + .../userguide/guide/zh-cn/about.install.md | 95 + .../userguide/guide/zh-cn/about.kohana.md | 15 + .../userguide/guide/zh-cn/about.mvc.md | 7 + .../userguide/guide/zh-cn/about.upgrading.md | 288 ++ .../userguide/guide/zh-cn/debugging.code.md | 18 + .../userguide/guide/zh-cn/debugging.errors.md | 22 + .../guide/zh-cn/debugging.profiling.md | 20 + .../modules/userguide/guide/zh-cn/features.md | 1 + .../modules/userguide/guide/zh-cn/menu.md | 31 + .../guide/zh-cn/security.validation.md | 244 ++ .../userguide/guide/zh-cn/security.xss.md | 15 + .../guide/zh-cn/tutorials.databases.md | 248 ++ .../userguide/guide/zh-cn/tutorials.git.md | 143 + .../guide/zh-cn/tutorials.helloworld.md | 106 + .../userguide/guide/zh-cn/tutorials.orm.md | 299 ++ .../guide/zh-cn/tutorials.removeindex.md | 89 + .../userguide/guide/zh-cn/tutorials.urls.md | 160 + .../guide/zh-cn/using.autoloading.md | 95 + .../guide/zh-cn/using.configuration.md | 57 + .../userguide/guide/zh-cn/using.messages.md | 26 + .../userguide/guide/zh-cn/using.sessions.md | 223 ++ .../userguide/guide/zh-cn/using.views.md | 118 + includes/kohana/modules/userguide/i18n/de.php | 6 + includes/kohana/modules/userguide/i18n/es.php | 6 + includes/kohana/modules/userguide/i18n/fr.php | 7 + includes/kohana/modules/userguide/i18n/he.php | 6 + includes/kohana/modules/userguide/i18n/nl.php | 6 + includes/kohana/modules/userguide/i18n/ru.php | 7 + includes/kohana/modules/userguide/i18n/zh.php | 28 + includes/kohana/modules/userguide/init.php | 30 + .../modules/userguide/media/css/kodoc.css | 62 + .../modules/userguide/media/css/print.css | 46 + .../modules/userguide/media/css/screen.css | 286 ++ .../modules/userguide/media/css/shCore.css | 330 ++ .../userguide/media/css/shThemeDefault.css | 173 + .../userguide/media/css/shThemeKodoc.css | 183 ++ .../media/img/cascading_filesystem.png | Bin 0 -> 59970 bytes .../userguide/media/img/hello_world_1.png | Bin 0 -> 1423 bytes .../userguide/media/img/hello_world_2.png | Bin 0 -> 6681 bytes .../media/img/hello_world_2_error.png | Bin 0 -> 68725 bytes .../modules/userguide/media/img/install.png | Bin 0 -> 75718 bytes .../modules/userguide/media/img/note.png | Bin 0 -> 199 bytes .../modules/userguide/media/img/welcome.png | Bin 0 -> 5057 bytes .../modules/userguide/media/js/jquery.min.js | 154 + .../modules/userguide/media/js/kodoc.js | 94 + .../modules/userguide/media/js/shBrushPhp.js | 91 + .../modules/userguide/media/js/shCore.js | 30 + .../modules/userguide/messages/userguide.php | 14 + .../userguide/vendor/markdown/License.text | 36 + .../userguide/vendor/markdown/markdown.php | 2909 +++++++++++++++++ .../userguide/views/userguide/api/class.php | 93 + .../userguide/views/userguide/api/menu.php | 16 + .../userguide/views/userguide/api/method.php | 52 + .../userguide/views/userguide/api/tags.php | 8 + .../userguide/views/userguide/api/toc.php | 16 + .../userguide/views/userguide/error.php | 3 + .../views/userguide/examples/error.php | 6 + .../userguide/examples/hello_world_error.php | 696 ++++ .../userguide/views/userguide/template.php | 89 + includes/kohana/system/base.php | 43 + includes/kohana/system/classes/arr.php | 3 + includes/kohana/system/classes/cli.php | 3 + includes/kohana/system/classes/controller.php | 3 + .../kohana/system/classes/controller/rest.php | 3 + .../system/classes/controller/template.php | 3 + includes/kohana/system/classes/cookie.php | 3 + includes/kohana/system/classes/date.php | 3 + includes/kohana/system/classes/encrypt.php | 3 + includes/kohana/system/classes/feed.php | 3 + includes/kohana/system/classes/file.php | 3 + includes/kohana/system/classes/form.php | 3 + includes/kohana/system/classes/fragment.php | 3 + includes/kohana/system/classes/html.php | 3 + includes/kohana/system/classes/i18n.php | 3 + includes/kohana/system/classes/inflector.php | 3 + includes/kohana/system/classes/kohana.php | 3 + includes/kohana/system/classes/kohana/arr.php | 470 +++ includes/kohana/system/classes/kohana/cli.php | 75 + .../kohana/system/classes/kohana/config.php | 153 + .../system/classes/kohana/config/file.php | 56 + .../system/classes/kohana/config/reader.php | 113 + .../system/classes/kohana/controller.php | 66 + .../system/classes/kohana/controller/rest.php | 74 + .../classes/kohana/controller/template.php | 50 + .../kohana/system/classes/kohana/cookie.php | 155 + .../kohana/system/classes/kohana/core.php | 1560 +++++++++ .../kohana/system/classes/kohana/date.php | 557 ++++ .../kohana/system/classes/kohana/encrypt.php | 211 ++ .../system/classes/kohana/exception.php | 46 + .../kohana/system/classes/kohana/feed.php | 176 + .../kohana/system/classes/kohana/file.php | 179 + .../kohana/system/classes/kohana/form.php | 434 +++ .../kohana/system/classes/kohana/fragment.php | 145 + .../kohana/system/classes/kohana/html.php | 367 +++ .../kohana/system/classes/kohana/i18n.php | 132 + .../system/classes/kohana/inflector.php | 237 ++ includes/kohana/system/classes/kohana/log.php | 185 ++ .../kohana/system/classes/kohana/log/file.php | 95 + .../system/classes/kohana/log/syslog.php | 65 + .../system/classes/kohana/log/writer.php | 35 + .../kohana/system/classes/kohana/model.php | 58 + includes/kohana/system/classes/kohana/num.php | 81 + .../kohana/system/classes/kohana/profiler.php | 375 +++ .../kohana/system/classes/kohana/remote.php | 152 + .../kohana/system/classes/kohana/request.php | 1221 +++++++ .../classes/kohana/request/exception.php | 9 + .../kohana/system/classes/kohana/route.php | 416 +++ .../kohana/system/classes/kohana/security.php | 193 ++ .../kohana/system/classes/kohana/session.php | 415 +++ .../system/classes/kohana/session/cookie.php | 34 + .../system/classes/kohana/session/native.php | 68 + .../kohana/system/classes/kohana/text.php | 590 ++++ .../kohana/system/classes/kohana/upload.php | 205 ++ includes/kohana/system/classes/kohana/url.php | 175 + .../kohana/system/classes/kohana/utf8.php | 761 +++++ .../kohana/system/classes/kohana/validate.php | 1161 +++++++ .../classes/kohana/validate/exception.php | 23 + .../kohana/system/classes/kohana/view.php | 346 ++ .../system/classes/kohana/view/exception.php | 9 + includes/kohana/system/classes/model.php | 3 + includes/kohana/system/classes/num.php | 3 + includes/kohana/system/classes/profiler.php | 3 + includes/kohana/system/classes/remote.php | 3 + includes/kohana/system/classes/request.php | 3 + includes/kohana/system/classes/route.php | 3 + includes/kohana/system/classes/security.php | 3 + includes/kohana/system/classes/session.php | 3 + .../kohana/system/classes/session/cookie.php | 3 + .../kohana/system/classes/session/native.php | 3 + includes/kohana/system/classes/text.php | 3 + includes/kohana/system/classes/upload.php | 3 + includes/kohana/system/classes/url.php | 3 + includes/kohana/system/classes/utf8.php | 3 + includes/kohana/system/classes/validate.php | 3 + .../system/classes/validate/exception.php | 3 + includes/kohana/system/classes/view.php | 3 + .../kohana/system/config/credit_cards.php | 60 + includes/kohana/system/config/encrypt.php | 17 + includes/kohana/system/config/inflector.php | 61 + includes/kohana/system/config/mimes.php | 225 ++ includes/kohana/system/config/session.php | 7 + includes/kohana/system/config/user_agents.php | 104 + includes/kohana/system/i18n/en.php | 3 + includes/kohana/system/i18n/es.php | 7 + includes/kohana/system/i18n/fr.php | 7 + includes/kohana/system/messages/validate.php | 25 + .../kohana/system/tests/kohana/ArrTest.php | 461 +++ .../kohana/system/tests/kohana/CLITest.php | 167 + .../kohana/system/tests/kohana/ConfigTest.php | 243 ++ .../kohana/system/tests/kohana/CookieTest.php | 130 + .../kohana/system/tests/kohana/CoreTest.php | 462 +++ .../kohana/system/tests/kohana/DateTest.php | 679 ++++ .../kohana/system/tests/kohana/FeedTest.php | 121 + .../kohana/system/tests/kohana/FileTest.php | 73 + .../kohana/system/tests/kohana/FormTest.php | 372 +++ .../kohana/system/tests/kohana/HTMLTest.php | 227 ++ .../kohana/system/tests/kohana/I18nTest.php | 73 + .../system/tests/kohana/InflectorTest.php | 142 + .../kohana/system/tests/kohana/LogTest.php | 87 + .../kohana/system/tests/kohana/ModelTest.php | 30 + .../kohana/system/tests/kohana/NumTest.php | 95 + .../kohana/system/tests/kohana/RemoteTest.php | 77 + .../system/tests/kohana/RequestTest.php | 191 ++ .../kohana/system/tests/kohana/RouteTest.php | 416 +++ .../system/tests/kohana/SecurityTest.php | 105 + .../system/tests/kohana/SessionTest.php | 497 +++ .../kohana/system/tests/kohana/TextTest.php | 572 ++++ .../kohana/system/tests/kohana/URLTest.php | 223 ++ .../kohana/system/tests/kohana/UTF8Test.php | 156 + .../kohana/system/tests/kohana/UploadTest.php | 223 ++ .../system/tests/kohana/ValidateTest.php | 1274 ++++++++ .../kohana/system/tests/test_data/github.png | Bin 0 -> 5101 bytes includes/kohana/system/utf8/from_unicode.php | 68 + includes/kohana/system/utf8/ltrim.php | 22 + includes/kohana/system/utf8/ord.php | 76 + includes/kohana/system/utf8/rtrim.php | 22 + includes/kohana/system/utf8/str_ireplace.php | 70 + includes/kohana/system/utf8/str_pad.php | 50 + includes/kohana/system/utf8/str_split.php | 27 + includes/kohana/system/utf8/strcasecmp.php | 19 + includes/kohana/system/utf8/strcspn.php | 30 + includes/kohana/system/utf8/stristr.php | 28 + includes/kohana/system/utf8/strlen.php | 17 + includes/kohana/system/utf8/strpos.php | 27 + includes/kohana/system/utf8/strrev.php | 18 + includes/kohana/system/utf8/strrpos.php | 27 + includes/kohana/system/utf8/strspn.php | 30 + includes/kohana/system/utf8/strtolower.php | 81 + includes/kohana/system/utf8/strtoupper.php | 81 + includes/kohana/system/utf8/substr.php | 72 + .../kohana/system/utf8/substr_replace.php | 22 + includes/kohana/system/utf8/to_unicode.php | 141 + .../system/utf8/transliterate_to_ascii.php | 77 + includes/kohana/system/utf8/trim.php | 17 + includes/kohana/system/utf8/ucfirst.php | 18 + includes/kohana/system/utf8/ucwords.php | 23 + includes/kohana/system/views/kohana/error.php | 128 + .../system/views/kohana/generate_logo.php | 14 + includes/kohana/system/views/kohana/logo.php | 8 + .../kohana/system/views/profiler/stats.php | 74 + .../kohana/system/views/profiler/style.css | 27 + kh.php | 103 + 558 files changed, 58712 insertions(+) create mode 100644 .htaccess create mode 100644 application/bootstrap.php create mode 100644 application/classes/controller/welcome.php create mode 100644 includes/kohana/LICENSE.md create mode 100644 includes/kohana/README.md create mode 100644 includes/kohana/install.php create mode 100644 includes/kohana/modules/auth/classes/auth.php create mode 100644 includes/kohana/modules/auth/classes/auth/file.php create mode 100644 includes/kohana/modules/auth/classes/auth/orm.php create mode 100644 includes/kohana/modules/auth/classes/kohana/auth.php create mode 100644 includes/kohana/modules/auth/classes/kohana/auth/file.php create mode 100644 includes/kohana/modules/auth/classes/kohana/auth/orm.php create mode 100644 includes/kohana/modules/auth/classes/model/auth/role.php create mode 100644 includes/kohana/modules/auth/classes/model/auth/user.php create mode 100644 includes/kohana/modules/auth/classes/model/auth/user/token.php create mode 100644 includes/kohana/modules/auth/classes/model/role.php create mode 100644 includes/kohana/modules/auth/classes/model/user.php create mode 100644 includes/kohana/modules/auth/classes/model/user/token.php create mode 100644 includes/kohana/modules/auth/config/auth.php create mode 100644 includes/kohana/modules/auth/mysql.sql create mode 100644 includes/kohana/modules/auth/postgresql.sql create mode 100644 includes/kohana/modules/cache/README.md create mode 100644 includes/kohana/modules/cache/classes/cache.php create mode 100644 includes/kohana/modules/cache/classes/cache/apc.php create mode 100644 includes/kohana/modules/cache/classes/cache/eaccelerator.php create mode 100644 includes/kohana/modules/cache/classes/cache/file.php create mode 100644 includes/kohana/modules/cache/classes/cache/memcache.php create mode 100644 includes/kohana/modules/cache/classes/cache/memcachetag.php create mode 100644 includes/kohana/modules/cache/classes/cache/sqlite.php create mode 100644 includes/kohana/modules/cache/classes/cache/xcache.php create mode 100644 includes/kohana/modules/cache/classes/kohana/cache.php create mode 100644 includes/kohana/modules/cache/classes/kohana/cache/apc.php create mode 100644 includes/kohana/modules/cache/classes/kohana/cache/eaccelerator.php create mode 100644 includes/kohana/modules/cache/classes/kohana/cache/exception.php create mode 100644 includes/kohana/modules/cache/classes/kohana/cache/file.php create mode 100644 includes/kohana/modules/cache/classes/kohana/cache/garbagecollect.php create mode 100644 includes/kohana/modules/cache/classes/kohana/cache/memcache.php create mode 100644 includes/kohana/modules/cache/classes/kohana/cache/memcachetag.php create mode 100644 includes/kohana/modules/cache/classes/kohana/cache/sqlite.php create mode 100644 includes/kohana/modules/cache/classes/kohana/cache/tagging.php create mode 100644 includes/kohana/modules/cache/classes/kohana/cache/xcache.php create mode 100644 includes/kohana/modules/cache/config/cache.php create mode 100644 includes/kohana/modules/cache/guide/cache.about.md create mode 100644 includes/kohana/modules/cache/guide/cache.config.md create mode 100644 includes/kohana/modules/cache/guide/cache.usage.md create mode 100644 includes/kohana/modules/cache/guide/menu.cache.md create mode 100644 includes/kohana/modules/cache/tests/cache/KohanaCacheTest.php create mode 100644 includes/kohana/modules/cache/tests/phpunit.xml create mode 100644 includes/kohana/modules/codebench/classes/bench/arrcallback.php create mode 100644 includes/kohana/modules/codebench/classes/bench/autolinkemails.php create mode 100644 includes/kohana/modules/codebench/classes/bench/datespan.php create mode 100644 includes/kohana/modules/codebench/classes/bench/explodelimit.php create mode 100644 includes/kohana/modules/codebench/classes/bench/gruberurl.php create mode 100644 includes/kohana/modules/codebench/classes/bench/ltrimdigits.php create mode 100644 includes/kohana/modules/codebench/classes/bench/mddobaseurl.php create mode 100644 includes/kohana/modules/codebench/classes/bench/mddoimageurl.php create mode 100644 includes/kohana/modules/codebench/classes/bench/mddoincludeviews.php create mode 100644 includes/kohana/modules/codebench/classes/bench/stripnullbytes.php create mode 100644 includes/kohana/modules/codebench/classes/bench/transliterate.php create mode 100644 includes/kohana/modules/codebench/classes/bench/urlsite.php create mode 100644 includes/kohana/modules/codebench/classes/bench/userfuncarray.php create mode 100644 includes/kohana/modules/codebench/classes/bench/validcolor.php create mode 100644 includes/kohana/modules/codebench/classes/bench/validurl.php create mode 100644 includes/kohana/modules/codebench/classes/codebench.php create mode 100644 includes/kohana/modules/codebench/classes/controller/codebench.php create mode 100644 includes/kohana/modules/codebench/classes/kohana/codebench.php create mode 100644 includes/kohana/modules/codebench/config/codebench.php create mode 100644 includes/kohana/modules/codebench/init.php create mode 100644 includes/kohana/modules/codebench/views/codebench.php create mode 100644 includes/kohana/modules/database/classes/database.php create mode 100644 includes/kohana/modules/database/classes/database/exception.php create mode 100644 includes/kohana/modules/database/classes/database/expression.php create mode 100644 includes/kohana/modules/database/classes/database/mysql.php create mode 100644 includes/kohana/modules/database/classes/database/mysql/result.php create mode 100644 includes/kohana/modules/database/classes/database/pdo.php create mode 100644 includes/kohana/modules/database/classes/database/query.php create mode 100644 includes/kohana/modules/database/classes/database/query/builder.php create mode 100644 includes/kohana/modules/database/classes/database/query/builder/delete.php create mode 100644 includes/kohana/modules/database/classes/database/query/builder/insert.php create mode 100644 includes/kohana/modules/database/classes/database/query/builder/join.php create mode 100644 includes/kohana/modules/database/classes/database/query/builder/select.php create mode 100644 includes/kohana/modules/database/classes/database/query/builder/update.php create mode 100644 includes/kohana/modules/database/classes/database/query/builder/where.php create mode 100644 includes/kohana/modules/database/classes/database/result.php create mode 100644 includes/kohana/modules/database/classes/database/result/cached.php create mode 100644 includes/kohana/modules/database/classes/db.php create mode 100644 includes/kohana/modules/database/classes/kohana/config/database.php create mode 100644 includes/kohana/modules/database/classes/kohana/database.php create mode 100644 includes/kohana/modules/database/classes/kohana/database/exception.php create mode 100644 includes/kohana/modules/database/classes/kohana/database/expression.php create mode 100644 includes/kohana/modules/database/classes/kohana/database/mysql.php create mode 100644 includes/kohana/modules/database/classes/kohana/database/mysql/result.php create mode 100644 includes/kohana/modules/database/classes/kohana/database/pdo.php create mode 100644 includes/kohana/modules/database/classes/kohana/database/query.php create mode 100644 includes/kohana/modules/database/classes/kohana/database/query/builder.php create mode 100644 includes/kohana/modules/database/classes/kohana/database/query/builder/delete.php create mode 100644 includes/kohana/modules/database/classes/kohana/database/query/builder/insert.php create mode 100644 includes/kohana/modules/database/classes/kohana/database/query/builder/join.php create mode 100644 includes/kohana/modules/database/classes/kohana/database/query/builder/select.php create mode 100644 includes/kohana/modules/database/classes/kohana/database/query/builder/update.php create mode 100644 includes/kohana/modules/database/classes/kohana/database/query/builder/where.php create mode 100644 includes/kohana/modules/database/classes/kohana/database/result.php create mode 100644 includes/kohana/modules/database/classes/kohana/database/result/cached.php create mode 100644 includes/kohana/modules/database/classes/kohana/db.php create mode 100644 includes/kohana/modules/database/classes/kohana/session/database.php create mode 100644 includes/kohana/modules/database/classes/session/database.php create mode 100644 includes/kohana/modules/database/config/database.php create mode 100644 includes/kohana/modules/database/config/session.php create mode 100644 includes/kohana/modules/image/README.markdown create mode 100644 includes/kohana/modules/image/classes/image.php create mode 100644 includes/kohana/modules/image/classes/image/gd.php create mode 100644 includes/kohana/modules/image/classes/kohana/image.php create mode 100644 includes/kohana/modules/image/classes/kohana/image/gd.php create mode 100755 includes/kohana/modules/oauth/README.md create mode 100755 includes/kohana/modules/oauth/classes/kohana/oauth.php create mode 100755 includes/kohana/modules/oauth/classes/kohana/oauth/consumer.php create mode 100755 includes/kohana/modules/oauth/classes/kohana/oauth/exception.php create mode 100644 includes/kohana/modules/oauth/classes/kohana/oauth/provider.php create mode 100644 includes/kohana/modules/oauth/classes/kohana/oauth/provider/google.php create mode 100644 includes/kohana/modules/oauth/classes/kohana/oauth/provider/twitter.php create mode 100755 includes/kohana/modules/oauth/classes/kohana/oauth/request.php create mode 100755 includes/kohana/modules/oauth/classes/kohana/oauth/request/access.php create mode 100755 includes/kohana/modules/oauth/classes/kohana/oauth/request/authorize.php create mode 100755 includes/kohana/modules/oauth/classes/kohana/oauth/request/credentials.php create mode 100755 includes/kohana/modules/oauth/classes/kohana/oauth/request/resource.php create mode 100644 includes/kohana/modules/oauth/classes/kohana/oauth/request/token.php create mode 100755 includes/kohana/modules/oauth/classes/kohana/oauth/response.php create mode 100755 includes/kohana/modules/oauth/classes/kohana/oauth/server.php create mode 100755 includes/kohana/modules/oauth/classes/kohana/oauth/signature.php create mode 100644 includes/kohana/modules/oauth/classes/kohana/oauth/signature/hmac/sha1.php create mode 100644 includes/kohana/modules/oauth/classes/kohana/oauth/signature/plaintext.php create mode 100755 includes/kohana/modules/oauth/classes/kohana/oauth/token.php create mode 100644 includes/kohana/modules/oauth/classes/kohana/oauth/token/access.php create mode 100644 includes/kohana/modules/oauth/classes/kohana/oauth/token/request.php create mode 100644 includes/kohana/modules/oauth/classes/oauth.php create mode 100644 includes/kohana/modules/oauth/classes/oauth/consumer.php create mode 100644 includes/kohana/modules/oauth/classes/oauth/provider.php create mode 100644 includes/kohana/modules/oauth/classes/oauth/provider/google.php create mode 100644 includes/kohana/modules/oauth/classes/oauth/provider/twitter.php create mode 100644 includes/kohana/modules/oauth/classes/oauth/request.php create mode 100644 includes/kohana/modules/oauth/classes/oauth/request/access.php create mode 100644 includes/kohana/modules/oauth/classes/oauth/request/authorize.php create mode 100644 includes/kohana/modules/oauth/classes/oauth/request/credentials.php create mode 100644 includes/kohana/modules/oauth/classes/oauth/request/resource.php create mode 100644 includes/kohana/modules/oauth/classes/oauth/request/token.php create mode 100644 includes/kohana/modules/oauth/classes/oauth/response.php create mode 100644 includes/kohana/modules/oauth/classes/oauth/signature.php create mode 100644 includes/kohana/modules/oauth/classes/oauth/signature/hmac/sha1.php create mode 100644 includes/kohana/modules/oauth/classes/oauth/signature/plaintext.php create mode 100644 includes/kohana/modules/oauth/classes/oauth/token.php create mode 100644 includes/kohana/modules/oauth/classes/oauth/token/access.php create mode 100644 includes/kohana/modules/oauth/classes/oauth/token/request.php create mode 100644 includes/kohana/modules/oauth/config/oauth.php create mode 100644 includes/kohana/modules/oauth/config/userguide.php create mode 100644 includes/kohana/modules/oauth/guide/oauth/config.md create mode 100644 includes/kohana/modules/oauth/guide/oauth/index.md create mode 100644 includes/kohana/modules/oauth/guide/oauth/menu.md create mode 100644 includes/kohana/modules/oauth/guide/oauth/usage.md create mode 100644 includes/kohana/modules/oauth/views/oauth/twitter/tweet.php create mode 100644 includes/kohana/modules/orm/classes/kohana/orm.php create mode 100644 includes/kohana/modules/orm/classes/orm.php create mode 100644 includes/kohana/modules/pagination/classes/kohana/pagination.php create mode 100644 includes/kohana/modules/pagination/classes/pagination.php create mode 100644 includes/kohana/modules/pagination/config/pagination.php create mode 100644 includes/kohana/modules/pagination/views/pagination/basic.php create mode 100644 includes/kohana/modules/pagination/views/pagination/floating.php create mode 100644 includes/kohana/modules/unittest/README.markdown create mode 100644 includes/kohana/modules/unittest/classes/controller/unittest.php create mode 100644 includes/kohana/modules/unittest/classes/kohana/tests.php create mode 100644 includes/kohana/modules/unittest/classes/kohana/unittest/helpers.php create mode 100644 includes/kohana/modules/unittest/classes/kohana/unittest/runner.php create mode 100644 includes/kohana/modules/unittest/classes/kohana/unittest/testcase.php create mode 100644 includes/kohana/modules/unittest/config/unittest.php create mode 100644 includes/kohana/modules/unittest/example.phpunit.xml create mode 100644 includes/kohana/modules/unittest/guide/menu.unittest.md create mode 100644 includes/kohana/modules/unittest/guide/unittest.mockobjects.md create mode 100644 includes/kohana/modules/unittest/guide/unittest.testing.md create mode 100644 includes/kohana/modules/unittest/guide/unittest.testing_workflows.md create mode 100644 includes/kohana/modules/unittest/guide/unittest.troubleshooting.md create mode 100644 includes/kohana/modules/unittest/init.php create mode 100644 includes/kohana/modules/unittest/tests.php create mode 100644 includes/kohana/modules/unittest/views/unittest/index.php create mode 100644 includes/kohana/modules/unittest/views/unittest/layout.php create mode 100644 includes/kohana/modules/unittest/views/unittest/results.php create mode 100644 includes/kohana/modules/userguide/README.md create mode 100644 includes/kohana/modules/userguide/classes/controller/userguide.php create mode 100644 includes/kohana/modules/userguide/classes/kodoc.php create mode 100644 includes/kohana/modules/userguide/classes/kodoc/class.php create mode 100644 includes/kohana/modules/userguide/classes/kodoc/markdown.php create mode 100644 includes/kohana/modules/userguide/classes/kodoc/method.php create mode 100644 includes/kohana/modules/userguide/classes/kodoc/method/param.php create mode 100644 includes/kohana/modules/userguide/classes/kodoc/missing.php create mode 100644 includes/kohana/modules/userguide/classes/kodoc/property.php create mode 100644 includes/kohana/modules/userguide/classes/kohana/kodoc.php create mode 100644 includes/kohana/modules/userguide/classes/kohana/kodoc/class.php create mode 100644 includes/kohana/modules/userguide/classes/kohana/kodoc/markdown.php create mode 100644 includes/kohana/modules/userguide/classes/kohana/kodoc/method.php create mode 100644 includes/kohana/modules/userguide/classes/kohana/kodoc/method/param.php create mode 100644 includes/kohana/modules/userguide/classes/kohana/kodoc/missing.php create mode 100644 includes/kohana/modules/userguide/classes/kohana/kodoc/property.php create mode 100644 includes/kohana/modules/userguide/config/userguide.php create mode 100644 includes/kohana/modules/userguide/guide/about.conventions.md create mode 100644 includes/kohana/modules/userguide/guide/about.filesystem.md create mode 100644 includes/kohana/modules/userguide/guide/about.flow.md create mode 100644 includes/kohana/modules/userguide/guide/about.install.md create mode 100644 includes/kohana/modules/userguide/guide/about.kohana.md create mode 100644 includes/kohana/modules/userguide/guide/about.mvc.md create mode 100644 includes/kohana/modules/userguide/guide/about.translation.md create mode 100644 includes/kohana/modules/userguide/guide/about.upgrading.md create mode 100644 includes/kohana/modules/userguide/guide/de-de/about.conventions.md create mode 100644 includes/kohana/modules/userguide/guide/de-de/about.kohana.md create mode 100644 includes/kohana/modules/userguide/guide/de-de/menu.md create mode 100644 includes/kohana/modules/userguide/guide/debugging.code.md create mode 100644 includes/kohana/modules/userguide/guide/debugging.errors.md create mode 100644 includes/kohana/modules/userguide/guide/debugging.profiling.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/debugging.errors.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/debugging.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/debugging.profiling.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/features.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/menu.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/security.cookies.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/security.database.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/security.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/security.validation.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/security.xss.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/start.autoloading.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/start.configuration.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/start.conventions.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/start.filesystem.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/start.flow.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/start.installation.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/start.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/start.mvc.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/tutorials.databases.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/tutorials.helloworld.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/tutorials.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/tutorials.urls.md create mode 100644 includes/kohana/modules/userguide/guide/es-es/upgrading.md create mode 100644 includes/kohana/modules/userguide/guide/features.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/about.autoloading.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/about.configuration.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/about.conventions.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/about.filesystem.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/about.flow.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/about.install.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/about.kohana.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/about.mvc.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/about.upgrading.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/debugging.code.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/debugging.errors.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/debugging.profiling.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/features.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/menu.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/security.cookies.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/security.database.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/security.validation.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/security.xss.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/tutorials.databases.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/tutorials.git.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/tutorials.helloworld.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/tutorials.orm.md create mode 100644 includes/kohana/modules/userguide/guide/fr-fr/tutorials.urls.md create mode 100644 includes/kohana/modules/userguide/guide/he-il/about.autoloading.md create mode 100644 includes/kohana/modules/userguide/guide/he-il/about.configuration.md create mode 100644 includes/kohana/modules/userguide/guide/he-il/about.filesystem.md create mode 100644 includes/kohana/modules/userguide/guide/he-il/about.flow.md create mode 100644 includes/kohana/modules/userguide/guide/he-il/about.kohana.md create mode 100644 includes/kohana/modules/userguide/guide/he-il/about.mvc.md create mode 100644 includes/kohana/modules/userguide/guide/he-il/debugging.code.md create mode 100644 includes/kohana/modules/userguide/guide/he-il/debugging.errors.md create mode 100644 includes/kohana/modules/userguide/guide/he-il/debugging.profiling.md create mode 100644 includes/kohana/modules/userguide/guide/he-il/menu.md create mode 100644 includes/kohana/modules/userguide/guide/menu.md create mode 100644 includes/kohana/modules/userguide/guide/nl/about.conventions.md create mode 100644 includes/kohana/modules/userguide/guide/nl/about.filesystem.md create mode 100644 includes/kohana/modules/userguide/guide/nl/about.flow.md create mode 100644 includes/kohana/modules/userguide/guide/nl/about.install.md create mode 100644 includes/kohana/modules/userguide/guide/nl/about.kohana.md create mode 100644 includes/kohana/modules/userguide/guide/nl/about.mvc.md create mode 100644 includes/kohana/modules/userguide/guide/nl/about.translation.md create mode 100644 includes/kohana/modules/userguide/guide/nl/about.upgrading.md create mode 100644 includes/kohana/modules/userguide/guide/nl/debugging.code.md create mode 100644 includes/kohana/modules/userguide/guide/nl/debugging.errors.md create mode 100644 includes/kohana/modules/userguide/guide/nl/debugging.profiling.md create mode 100644 includes/kohana/modules/userguide/guide/nl/features.md create mode 100644 includes/kohana/modules/userguide/guide/nl/menu.md create mode 100644 includes/kohana/modules/userguide/guide/nl/security.cookies.md create mode 100644 includes/kohana/modules/userguide/guide/nl/security.database.md create mode 100644 includes/kohana/modules/userguide/guide/nl/security.validation.md create mode 100644 includes/kohana/modules/userguide/guide/nl/security.xss.md create mode 100644 includes/kohana/modules/userguide/guide/nl/tutorials.databases.md create mode 100644 includes/kohana/modules/userguide/guide/nl/tutorials.git.md create mode 100644 includes/kohana/modules/userguide/guide/nl/tutorials.helloworld.md create mode 100644 includes/kohana/modules/userguide/guide/nl/tutorials.orm.md create mode 100644 includes/kohana/modules/userguide/guide/nl/tutorials.removeindex.md create mode 100644 includes/kohana/modules/userguide/guide/nl/tutorials.urls.md create mode 100644 includes/kohana/modules/userguide/guide/nl/using.autoloading.md create mode 100644 includes/kohana/modules/userguide/guide/nl/using.configuration.md create mode 100644 includes/kohana/modules/userguide/guide/nl/using.messages.md create mode 100644 includes/kohana/modules/userguide/guide/nl/using.sessions.md create mode 100644 includes/kohana/modules/userguide/guide/nl/using.views.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/about.conventions.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/about.filesystem.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/about.flow.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/about.install.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/about.kohana.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/about.mvc.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/about.upgrading.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/debugging.code.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/debugging.errors.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/debugging.profiling.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/features.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/menu.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/security.cookies.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/security.database.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/security.validation.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/security.xss.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/start.autoloading.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/start.configuration.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/start.controllers.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/start.conventions.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/start.filesystem.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/start.flow.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/start.installation.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/start.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/start.mvc.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/tutorials.databases.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/tutorials.git.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/tutorials.helloworld.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/tutorials.orm.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/tutorials.removeindex.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/tutorials.urls.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/using.autoloading.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/using.configuration.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/using.messages.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/using.sessions.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/using.translation.md create mode 100644 includes/kohana/modules/userguide/guide/ru-ru/using.views.md create mode 100644 includes/kohana/modules/userguide/guide/security.cookies.md create mode 100644 includes/kohana/modules/userguide/guide/security.database.md create mode 100644 includes/kohana/modules/userguide/guide/security.validation.md create mode 100644 includes/kohana/modules/userguide/guide/security.xss.md create mode 100644 includes/kohana/modules/userguide/guide/tutorials.databases.md create mode 100644 includes/kohana/modules/userguide/guide/tutorials.git.md create mode 100644 includes/kohana/modules/userguide/guide/tutorials.helloworld.md create mode 100644 includes/kohana/modules/userguide/guide/tutorials.orm.md create mode 100644 includes/kohana/modules/userguide/guide/tutorials.removeindex.md create mode 100755 includes/kohana/modules/userguide/guide/tutorials.urls.md create mode 100644 includes/kohana/modules/userguide/guide/using.autoloading.md create mode 100644 includes/kohana/modules/userguide/guide/using.configuration.md create mode 100644 includes/kohana/modules/userguide/guide/using.messages.md create mode 100644 includes/kohana/modules/userguide/guide/using.sessions.md create mode 100644 includes/kohana/modules/userguide/guide/using.views.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/about.conventions.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/about.filesystem.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/about.flow.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/about.install.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/about.kohana.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/about.mvc.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/about.upgrading.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/debugging.code.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/debugging.errors.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/debugging.profiling.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/features.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/menu.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/security.validation.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/security.xss.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/tutorials.databases.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/tutorials.git.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/tutorials.helloworld.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/tutorials.orm.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/tutorials.removeindex.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/tutorials.urls.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/using.autoloading.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/using.configuration.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/using.messages.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/using.sessions.md create mode 100644 includes/kohana/modules/userguide/guide/zh-cn/using.views.md create mode 100644 includes/kohana/modules/userguide/i18n/de.php create mode 100644 includes/kohana/modules/userguide/i18n/es.php create mode 100644 includes/kohana/modules/userguide/i18n/fr.php create mode 100644 includes/kohana/modules/userguide/i18n/he.php create mode 100644 includes/kohana/modules/userguide/i18n/nl.php create mode 100644 includes/kohana/modules/userguide/i18n/ru.php create mode 100644 includes/kohana/modules/userguide/i18n/zh.php create mode 100644 includes/kohana/modules/userguide/init.php create mode 100644 includes/kohana/modules/userguide/media/css/kodoc.css create mode 100644 includes/kohana/modules/userguide/media/css/print.css create mode 100644 includes/kohana/modules/userguide/media/css/screen.css create mode 100644 includes/kohana/modules/userguide/media/css/shCore.css create mode 100644 includes/kohana/modules/userguide/media/css/shThemeDefault.css create mode 100644 includes/kohana/modules/userguide/media/css/shThemeKodoc.css create mode 100644 includes/kohana/modules/userguide/media/img/cascading_filesystem.png create mode 100644 includes/kohana/modules/userguide/media/img/hello_world_1.png create mode 100644 includes/kohana/modules/userguide/media/img/hello_world_2.png create mode 100644 includes/kohana/modules/userguide/media/img/hello_world_2_error.png create mode 100644 includes/kohana/modules/userguide/media/img/install.png create mode 100644 includes/kohana/modules/userguide/media/img/note.png create mode 100644 includes/kohana/modules/userguide/media/img/welcome.png create mode 100644 includes/kohana/modules/userguide/media/js/jquery.min.js create mode 100644 includes/kohana/modules/userguide/media/js/kodoc.js create mode 100644 includes/kohana/modules/userguide/media/js/shBrushPhp.js create mode 100644 includes/kohana/modules/userguide/media/js/shCore.js create mode 100644 includes/kohana/modules/userguide/messages/userguide.php create mode 100755 includes/kohana/modules/userguide/vendor/markdown/License.text create mode 100755 includes/kohana/modules/userguide/vendor/markdown/markdown.php create mode 100644 includes/kohana/modules/userguide/views/userguide/api/class.php create mode 100644 includes/kohana/modules/userguide/views/userguide/api/menu.php create mode 100644 includes/kohana/modules/userguide/views/userguide/api/method.php create mode 100644 includes/kohana/modules/userguide/views/userguide/api/tags.php create mode 100644 includes/kohana/modules/userguide/views/userguide/api/toc.php create mode 100644 includes/kohana/modules/userguide/views/userguide/error.php create mode 100644 includes/kohana/modules/userguide/views/userguide/examples/error.php create mode 100644 includes/kohana/modules/userguide/views/userguide/examples/hello_world_error.php create mode 100644 includes/kohana/modules/userguide/views/userguide/template.php create mode 100644 includes/kohana/system/base.php create mode 100644 includes/kohana/system/classes/arr.php create mode 100644 includes/kohana/system/classes/cli.php create mode 100644 includes/kohana/system/classes/controller.php create mode 100644 includes/kohana/system/classes/controller/rest.php create mode 100644 includes/kohana/system/classes/controller/template.php create mode 100644 includes/kohana/system/classes/cookie.php create mode 100644 includes/kohana/system/classes/date.php create mode 100644 includes/kohana/system/classes/encrypt.php create mode 100644 includes/kohana/system/classes/feed.php create mode 100644 includes/kohana/system/classes/file.php create mode 100644 includes/kohana/system/classes/form.php create mode 100644 includes/kohana/system/classes/fragment.php create mode 100644 includes/kohana/system/classes/html.php create mode 100644 includes/kohana/system/classes/i18n.php create mode 100644 includes/kohana/system/classes/inflector.php create mode 100644 includes/kohana/system/classes/kohana.php create mode 100644 includes/kohana/system/classes/kohana/arr.php create mode 100644 includes/kohana/system/classes/kohana/cli.php create mode 100644 includes/kohana/system/classes/kohana/config.php create mode 100644 includes/kohana/system/classes/kohana/config/file.php create mode 100644 includes/kohana/system/classes/kohana/config/reader.php create mode 100644 includes/kohana/system/classes/kohana/controller.php create mode 100644 includes/kohana/system/classes/kohana/controller/rest.php create mode 100644 includes/kohana/system/classes/kohana/controller/template.php create mode 100644 includes/kohana/system/classes/kohana/cookie.php create mode 100644 includes/kohana/system/classes/kohana/core.php create mode 100644 includes/kohana/system/classes/kohana/date.php create mode 100644 includes/kohana/system/classes/kohana/encrypt.php create mode 100644 includes/kohana/system/classes/kohana/exception.php create mode 100644 includes/kohana/system/classes/kohana/feed.php create mode 100644 includes/kohana/system/classes/kohana/file.php create mode 100644 includes/kohana/system/classes/kohana/form.php create mode 100644 includes/kohana/system/classes/kohana/fragment.php create mode 100644 includes/kohana/system/classes/kohana/html.php create mode 100644 includes/kohana/system/classes/kohana/i18n.php create mode 100644 includes/kohana/system/classes/kohana/inflector.php create mode 100644 includes/kohana/system/classes/kohana/log.php create mode 100644 includes/kohana/system/classes/kohana/log/file.php create mode 100644 includes/kohana/system/classes/kohana/log/syslog.php create mode 100644 includes/kohana/system/classes/kohana/log/writer.php create mode 100644 includes/kohana/system/classes/kohana/model.php create mode 100644 includes/kohana/system/classes/kohana/num.php create mode 100644 includes/kohana/system/classes/kohana/profiler.php create mode 100644 includes/kohana/system/classes/kohana/remote.php create mode 100644 includes/kohana/system/classes/kohana/request.php create mode 100644 includes/kohana/system/classes/kohana/request/exception.php create mode 100644 includes/kohana/system/classes/kohana/route.php create mode 100644 includes/kohana/system/classes/kohana/security.php create mode 100644 includes/kohana/system/classes/kohana/session.php create mode 100644 includes/kohana/system/classes/kohana/session/cookie.php create mode 100644 includes/kohana/system/classes/kohana/session/native.php create mode 100644 includes/kohana/system/classes/kohana/text.php create mode 100644 includes/kohana/system/classes/kohana/upload.php create mode 100644 includes/kohana/system/classes/kohana/url.php create mode 100644 includes/kohana/system/classes/kohana/utf8.php create mode 100644 includes/kohana/system/classes/kohana/validate.php create mode 100644 includes/kohana/system/classes/kohana/validate/exception.php create mode 100644 includes/kohana/system/classes/kohana/view.php create mode 100644 includes/kohana/system/classes/kohana/view/exception.php create mode 100644 includes/kohana/system/classes/model.php create mode 100644 includes/kohana/system/classes/num.php create mode 100644 includes/kohana/system/classes/profiler.php create mode 100644 includes/kohana/system/classes/remote.php create mode 100644 includes/kohana/system/classes/request.php create mode 100644 includes/kohana/system/classes/route.php create mode 100644 includes/kohana/system/classes/security.php create mode 100644 includes/kohana/system/classes/session.php create mode 100644 includes/kohana/system/classes/session/cookie.php create mode 100644 includes/kohana/system/classes/session/native.php create mode 100644 includes/kohana/system/classes/text.php create mode 100644 includes/kohana/system/classes/upload.php create mode 100644 includes/kohana/system/classes/url.php create mode 100644 includes/kohana/system/classes/utf8.php create mode 100644 includes/kohana/system/classes/validate.php create mode 100644 includes/kohana/system/classes/validate/exception.php create mode 100644 includes/kohana/system/classes/view.php create mode 100644 includes/kohana/system/config/credit_cards.php create mode 100644 includes/kohana/system/config/encrypt.php create mode 100644 includes/kohana/system/config/inflector.php create mode 100644 includes/kohana/system/config/mimes.php create mode 100644 includes/kohana/system/config/session.php create mode 100644 includes/kohana/system/config/user_agents.php create mode 100644 includes/kohana/system/i18n/en.php create mode 100644 includes/kohana/system/i18n/es.php create mode 100644 includes/kohana/system/i18n/fr.php create mode 100644 includes/kohana/system/messages/validate.php create mode 100644 includes/kohana/system/tests/kohana/ArrTest.php create mode 100644 includes/kohana/system/tests/kohana/CLITest.php create mode 100644 includes/kohana/system/tests/kohana/ConfigTest.php create mode 100644 includes/kohana/system/tests/kohana/CookieTest.php create mode 100644 includes/kohana/system/tests/kohana/CoreTest.php create mode 100644 includes/kohana/system/tests/kohana/DateTest.php create mode 100644 includes/kohana/system/tests/kohana/FeedTest.php create mode 100644 includes/kohana/system/tests/kohana/FileTest.php create mode 100644 includes/kohana/system/tests/kohana/FormTest.php create mode 100644 includes/kohana/system/tests/kohana/HTMLTest.php create mode 100644 includes/kohana/system/tests/kohana/I18nTest.php create mode 100644 includes/kohana/system/tests/kohana/InflectorTest.php create mode 100644 includes/kohana/system/tests/kohana/LogTest.php create mode 100644 includes/kohana/system/tests/kohana/ModelTest.php create mode 100644 includes/kohana/system/tests/kohana/NumTest.php create mode 100644 includes/kohana/system/tests/kohana/RemoteTest.php create mode 100644 includes/kohana/system/tests/kohana/RequestTest.php create mode 100644 includes/kohana/system/tests/kohana/RouteTest.php create mode 100644 includes/kohana/system/tests/kohana/SecurityTest.php create mode 100644 includes/kohana/system/tests/kohana/SessionTest.php create mode 100644 includes/kohana/system/tests/kohana/TextTest.php create mode 100644 includes/kohana/system/tests/kohana/URLTest.php create mode 100644 includes/kohana/system/tests/kohana/UTF8Test.php create mode 100644 includes/kohana/system/tests/kohana/UploadTest.php create mode 100644 includes/kohana/system/tests/kohana/ValidateTest.php create mode 100644 includes/kohana/system/tests/test_data/github.png create mode 100644 includes/kohana/system/utf8/from_unicode.php create mode 100644 includes/kohana/system/utf8/ltrim.php create mode 100644 includes/kohana/system/utf8/ord.php create mode 100644 includes/kohana/system/utf8/rtrim.php create mode 100644 includes/kohana/system/utf8/str_ireplace.php create mode 100644 includes/kohana/system/utf8/str_pad.php create mode 100644 includes/kohana/system/utf8/str_split.php create mode 100644 includes/kohana/system/utf8/strcasecmp.php create mode 100644 includes/kohana/system/utf8/strcspn.php create mode 100644 includes/kohana/system/utf8/stristr.php create mode 100644 includes/kohana/system/utf8/strlen.php create mode 100644 includes/kohana/system/utf8/strpos.php create mode 100644 includes/kohana/system/utf8/strrev.php create mode 100644 includes/kohana/system/utf8/strrpos.php create mode 100644 includes/kohana/system/utf8/strspn.php create mode 100644 includes/kohana/system/utf8/strtolower.php create mode 100644 includes/kohana/system/utf8/strtoupper.php create mode 100644 includes/kohana/system/utf8/substr.php create mode 100644 includes/kohana/system/utf8/substr_replace.php create mode 100644 includes/kohana/system/utf8/to_unicode.php create mode 100644 includes/kohana/system/utf8/transliterate_to_ascii.php create mode 100644 includes/kohana/system/utf8/trim.php create mode 100644 includes/kohana/system/utf8/ucfirst.php create mode 100644 includes/kohana/system/utf8/ucwords.php create mode 100644 includes/kohana/system/views/kohana/error.php create mode 100644 includes/kohana/system/views/kohana/generate_logo.php create mode 100644 includes/kohana/system/views/kohana/logo.php create mode 100755 includes/kohana/system/views/profiler/stats.php create mode 100644 includes/kohana/system/views/profiler/style.css create mode 100644 kh.php diff --git a/.htaccess b/.htaccess new file mode 100644 index 00000000..87b9cf81 --- /dev/null +++ b/.htaccess @@ -0,0 +1,21 @@ +# Turn on URL rewriting +RewriteEngine On + +# Installation directory +RewriteBase /kohana/ + +# Protect hidden files from being viewed + + Order Deny,Allow + Deny From All + + +# Protect application and system files from being viewed +RewriteRule ^(?:application|modules|system)\b.* index.php/$0 [L] + +# Allow any files or directories that exist to be displayed directly +RewriteCond %{REQUEST_FILENAME} !-f +RewriteCond %{REQUEST_FILENAME} !-d + +# Rewrite all other URLs to index.php/URL +RewriteRule .* index.php/$0 [PT] diff --git a/application/bootstrap.php b/application/bootstrap.php new file mode 100644 index 00000000..a76dfc07 --- /dev/null +++ b/application/bootstrap.php @@ -0,0 +1,102 @@ + '/', +)); + +/** + * Attach the file write to logging. Multiple writers are supported. + */ +Kohana::$log->attach(new Kohana_Log_File(APPPATH.'logs')); + +/** + * Attach a file reader to config. Multiple readers are supported. + */ +Kohana::$config->attach(new Kohana_Config_File); + +/** + * Enable modules. Modules are referenced by a relative or absolute path. + */ +Kohana::modules(array( + // 'auth' => MODPATH.'auth', // Basic authentication + // 'cache' => MODPATH.'cache', // Caching with multiple backends + // 'codebench' => MODPATH.'codebench', // Benchmarking tool + // 'database' => MODPATH.'database', // Database access + // 'image' => MODPATH.'image', // Image manipulation + // 'orm' => MODPATH.'orm', // Object Relationship Mapping + // 'oauth' => MODPATH.'oauth', // OAuth authentication + // 'pagination' => MODPATH.'pagination', // Paging of results + // 'unittest' => MODPATH.'unittest', // Unit testing + // 'userguide' => MODPATH.'userguide', // User guide and API documentation + )); + +/** + * Set the routes. Each route must have a minimum of a name, a URI and a set of + * defaults for the URI. + */ +Route::set('default', '((/(/)))') + ->defaults(array( + 'controller' => 'welcome', + 'action' => 'index', + )); + +if ( ! defined('SUPPRESS_REQUEST')) +{ + /** + * Execute the main request. A source of the URI can be passed, eg: $_SERVER['PATH_INFO']. + * If no source is specified, the URI will be automatically detected. + */ + echo Request::instance() + ->execute() + ->send_headers() + ->response; +} diff --git a/application/classes/controller/welcome.php b/application/classes/controller/welcome.php new file mode 100644 index 00000000..e3b27d60 --- /dev/null +++ b/application/classes/controller/welcome.php @@ -0,0 +1,10 @@ +request->response = 'hello, world!'; + } + +} // End Welcome diff --git a/includes/kohana/LICENSE.md b/includes/kohana/LICENSE.md new file mode 100644 index 00000000..87af9ad5 --- /dev/null +++ b/includes/kohana/LICENSE.md @@ -0,0 +1,14 @@ +# Kohana License Agreement + +This license is a legal agreement between you and the Kohana Team for the use of Kohana Framework (the "Software"). By obtaining the Software you agree to comply with the terms and conditions of this license. + +Copyright (c) 2007-2010 Kohana Team +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. +* Neither the name of the Kohana nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/includes/kohana/README.md b/includes/kohana/README.md new file mode 100644 index 00000000..97e31dc3 --- /dev/null +++ b/includes/kohana/README.md @@ -0,0 +1,3 @@ +# Kohana PHP Framework, version 3.0 (dev) + +This is the current development version of [Kohana](http://kohanaframework.org/). diff --git a/includes/kohana/install.php b/includes/kohana/install.php new file mode 100644 index 00000000..37d15300 --- /dev/null +++ b/includes/kohana/install.php @@ -0,0 +1,209 @@ + + + + + + + + Kohana Installation + + + + + + +

Environment Tests

+ +

+ The following tests have been run to determine if Kohana will work in your environment. + If any of the tests have failed, consult the documentation + for more information on how to correct the problem. +

+ + + + + + + =')): ?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PHP VersionKohana requires PHP 5.2.3 or newer, this version is .
System DirectoryThe configured system directory does not exist or does not contain required files.
Application DirectoryThe configured application directory does not exist or does not contain required files.
Cache DirectoryThe directory is not writable.
Logs DirectoryThe directory is not writable.
PCRE UTF-8PCRE has not been compiled with UTF-8 support.PCRE has not been compiled with Unicode property support.Pass
SPL EnabledPassPHP SPL is either not loaded or not compiled in.
Reflection EnabledPassPHP reflection is either not loaded or not compiled in.
Filters EnabledPassThe filter extension is either not loaded or not compiled in.
Iconv Extension LoadedPassThe iconv extension is not loaded.
Mbstring Not OverloadedThe mbstring extension is overloading PHP's native string functions.Pass
Character Type (CTYPE) ExtensionThe ctype extension is not enabled.Pass
URI DeterminationPassNeither $_SERVER['REQUEST_URI'], $_SERVER['PHP_SELF'], or $_SERVER['PATH_INFO'] is available.
+ + +

✘ Kohana may not work correctly with your environment.

+ +

✔ Your environment passed all requirements.
+ Remove or rename the install file now.

+ + +

Optional Tests

+ +

+ The following extensions are not required to run the Kohana core, but if enabled can provide access to additional classes. +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
cURL EnabledPassKohana requires cURL for the Remote class.
mcrypt EnabledPassKohana requires mcrypt for the Encrypt class.
GD EnabledPassKohana requires GD v2 for the Image class.
PDO EnabledPassKohana can use PDO to support additional databases.
+ + + diff --git a/includes/kohana/modules/auth/classes/auth.php b/includes/kohana/modules/auth/classes/auth.php new file mode 100644 index 00000000..a02b1e5f --- /dev/null +++ b/includes/kohana/modules/auth/classes/auth.php @@ -0,0 +1,3 @@ +get('driver')) + { + $type = 'ORM'; + } + + // Set the session class name + $class = 'Auth_'.ucfirst($type); + + // Create a new session instance + Auth::$_instance = new $class($config); + } + + return Auth::$_instance; + } + + /** + * Create an instance of Auth. + * + * @return Auth + */ + public static function factory($config = array()) + { + return new Auth($config); + } + + protected $_session; + + protected $_config; + + /** + * Loads Session and configuration options. + * + * @return void + */ + public function __construct($config = array()) + { + // Clean up the salt pattern and split it into an array + $config['salt_pattern'] = preg_split('/,\s*/', Kohana::config('auth')->get('salt_pattern')); + + // Save the config in the object + $this->_config = $config; + + $this->_session = Session::instance(); + } + + abstract protected function _login($username, $password, $remember); + + abstract public function password($username); + + abstract public function check_password($password); + + /** + * Gets the currently logged in user from the session. + * Returns FALSE if no user is currently logged in. + * + * @return mixed + */ + public function get_user() + { + return $this->_session->get($this->_config['session_key'], FALSE); + } + + /** + * Attempt to log in a user by using an ORM object and plain-text password. + * + * @param string username to log in + * @param string password to check against + * @param boolean enable autologin + * @return boolean + */ + public function login($username, $password, $remember = FALSE) + { + if (empty($password)) + return FALSE; + + if (is_string($password)) + { + // Get the salt from the stored password + $salt = $this->find_salt($this->password($username)); + + // Create a hashed password using the salt from the stored password + $password = $this->hash_password($password, $salt); + } + + return $this->_login($username, $password, $remember); + } + + /** + * Log out a user by removing the related session variables. + * + * @param boolean completely destroy the session + * @param boolean remove all tokens for user + * @return boolean + */ + public function logout($destroy = FALSE, $logout_all = FALSE) + { + if ($destroy === TRUE) + { + // Destroy the session completely + $this->_session->destroy(); + } + else + { + // Remove the user from the session + $this->_session->delete($this->_config['session_key']); + + // Regenerate session_id + $this->_session->regenerate(); + } + + // Double check + return ! $this->logged_in(); + } + + /** + * Check if there is an active session. Optionally allows checking for a + * specific role. + * + * @param string role name + * @return mixed + */ + public function logged_in($role = NULL) + { + return FALSE !== $this->get_user(); + } + + /** + * Creates a hashed password from a plaintext password, inserting salt + * based on the configured salt pattern. + * + * @param string plaintext password + * @return string hashed password string + */ + public function hash_password($password, $salt = FALSE) + { + if ($salt === FALSE) + { + // Create a salt seed, same length as the number of offsets in the pattern + $salt = substr($this->hash(uniqid(NULL, TRUE)), 0, count($this->_config['salt_pattern'])); + } + + // Password hash that the salt will be inserted into + $hash = $this->hash($salt.$password); + + // Change salt to an array + $salt = str_split($salt, 1); + + // Returned password + $password = ''; + + // Used to calculate the length of splits + $last_offset = 0; + + foreach ($this->_config['salt_pattern'] as $offset) + { + // Split a new part of the hash off + $part = substr($hash, 0, $offset - $last_offset); + + // Cut the current part out of the hash + $hash = substr($hash, $offset - $last_offset); + + // Add the part to the password, appending the salt character + $password .= $part.array_shift($salt); + + // Set the last offset to the current offset + $last_offset = $offset; + } + + // Return the password, with the remaining hash appended + return $password.$hash; + } + + /** + * Perform a hash, using the configured method. + * + * @param string string to hash + * @return string + */ + public function hash($str) + { + return hash($this->_config['hash_method'], $str); + } + + /** + * Finds the salt from a password, based on the configured salt pattern. + * + * @param string hashed password + * @return string + */ + public function find_salt($password) + { + $salt = ''; + + foreach ($this->_config['salt_pattern'] as $i => $offset) + { + // Find salt characters, take a good long look... + $salt .= substr($password, $offset + $i, 1); + } + + return $salt; + } + + protected function complete_login($user) + { + // Regenerate session_id + $this->_session->regenerate(); + + // Store username in session + $this->_session->set($this->_config['session_key'], $user); + + return TRUE; + } + +} // End Auth \ No newline at end of file diff --git a/includes/kohana/modules/auth/classes/kohana/auth/file.php b/includes/kohana/modules/auth/classes/kohana/auth/file.php new file mode 100644 index 00000000..439f17fa --- /dev/null +++ b/includes/kohana/modules/auth/classes/kohana/auth/file.php @@ -0,0 +1,88 @@ +_users = Arr::get($config, 'users', array()); + } + + /** + * Logs a user in. + * + * @param string username + * @param string password + * @param boolean enable autologin (not supported) + * @return boolean + */ + protected function _login($username, $password, $remember) + { + if (isset($this->_users[$username]) AND $this->_users[$username] === $password) + { + // Complete the login + return $this->complete_login($username); + } + + // Login failed + return FALSE; + } + + /** + * Forces a user to be logged in, without specifying a password. + * + * @param mixed username + * @return boolean + */ + public function force_login($username) + { + // Complete the login + return $this->complete_login($username); + } + + /** + * Get the stored password for a username. + * + * @param mixed username + * @return string + */ + public function password($username) + { + return Arr::get($this->_users, $username, FALSE); + } + + /** + * Compare password with original (plain text). Works for current (logged in) user + * + * @param string $password + * @return boolean + */ + public function check_password($password) + { + $username = $this->get_user(); + + if ($username === FALSE) + { + return FALSE; + } + + return ($password === $this->password($username)); + } + +} // End Auth File \ No newline at end of file diff --git a/includes/kohana/modules/auth/classes/kohana/auth/orm.php b/includes/kohana/modules/auth/classes/kohana/auth/orm.php new file mode 100644 index 00000000..bd3decbb --- /dev/null +++ b/includes/kohana/modules/auth/classes/kohana/auth/orm.php @@ -0,0 +1,288 @@ +get_user(); + + if (is_object($user) AND $user instanceof Model_User AND $user->loaded()) + { + // Everything is okay so far + $status = TRUE; + + if ( ! empty($role)) + { + // Multiple roles to check + if (is_array($role)) + { + // Check each role + foreach ($role as $_role) + { + if ( ! is_object($_role)) + { + $_role = ORM::factory('role', array('name' => $_role)); + } + + // If the user doesn't have the role + if ( ! $user->has('roles', $_role)) + { + // Set the status false and get outta here + $status = FALSE; + break; + } + } + } + // Single role to check + else + { + if ( ! is_object($role)) + { + // Load the role + $role = ORM::factory('role', array('name' => $role)); + } + + // Check that the user has the given role + $status = $user->has('roles', $role); + } + } + } + + return $status; + } + + /** + * Logs a user in. + * + * @param string username + * @param string password + * @param boolean enable autologin + * @return boolean + */ + protected function _login($user, $password, $remember) + { + if ( ! is_object($user)) + { + $username = $user; + + // Load the user + $user = ORM::factory('user'); + $user->where($user->unique_key($username), '=', $username)->find(); + } + + // If the passwords match, perform a login + if ($user->has('roles', ORM::factory('role', array('name' => 'login'))) AND $user->password === $password) + { + if ($remember === TRUE) + { + // Create a new autologin token + $token = ORM::factory('user_token'); + + // Set token data + $token->user_id = $user->id; + $token->expires = time() + $this->_config['lifetime']; + $token->save(); + + // Set the autologin cookie + Cookie::set('authautologin', $token->token, $this->_config['lifetime']); + } + + // Finish the login + $this->complete_login($user); + + return TRUE; + } + + // Login failed + return FALSE; + } + + /** + * Forces a user to be logged in, without specifying a password. + * + * @param mixed username string, or user ORM object + * @param boolean mark the session as forced + * @return boolean + */ + public function force_login($user, $mark_session_as_forced = FALSE) + { + if ( ! is_object($user)) + { + $username = $user; + + // Load the user + $user = ORM::factory('user'); + $user->where($user->unique_key($username), '=', $username)->find(); + } + + if ($mark_session_as_forced === TRUE) + { + // Mark the session as forced, to prevent users from changing account information + $this->_session->set('auth_forced', TRUE); + } + + // Run the standard completion + $this->complete_login($user); + } + + /** + * Logs a user in, based on the authautologin cookie. + * + * @return mixed + */ + public function auto_login() + { + if ($token = Cookie::get('authautologin')) + { + // Load the token and user + $token = ORM::factory('user_token', array('token' => $token)); + + if ($token->loaded() AND $token->user->loaded()) + { + if ($token->user_agent === sha1(Request::$user_agent)) + { + // Save the token to create a new unique token + $token->save(); + + // Set the new token + Cookie::set('authautologin', $token->token, $token->expires - time()); + + // Complete the login with the found data + $this->complete_login($token->user); + + // Automatic login was successful + return $token->user; + } + + // Token is invalid + $token->delete(); + } + } + + return FALSE; + } + + /** + * Gets the currently logged in user from the session (with auto_login check). + * Returns FALSE if no user is currently logged in. + * + * @return mixed + */ + public function get_user() + { + $user = parent::get_user(); + + if ($user === FALSE) + { + // check for "remembered" login + $user = $this->auto_login(); + } + + return $user; + } + + /** + * Log a user out and remove any autologin cookies. + * + * @param boolean completely destroy the session + * @param boolean remove all tokens for user + * @return boolean + */ + public function logout($destroy = FALSE, $logout_all = FALSE) + { + // Set by force_login() + $this->_session->delete('auth_forced'); + + if ($token = Cookie::get('authautologin')) + { + // Delete the autologin cookie to prevent re-login + Cookie::delete('authautologin'); + + // Clear the autologin token from the database + $token = ORM::factory('user_token', array('token' => $token)); + + if ($token->loaded() AND $logout_all) + { + ORM::factory('user_token')->where('user_id', '=', $token->user_id)->delete_all(); + } + elseif ($token->loaded()) + { + $token->delete(); + } + } + + return parent::logout($destroy); + } + + /** + * Get the stored password for a username. + * + * @param mixed username string, or user ORM object + * @return string + */ + public function password($user) + { + if ( ! is_object($user)) + { + $username = $user; + + // Load the user + $user = ORM::factory('user'); + $user->where($user->unique_key($username), '=', $username)->find(); + } + + return $user->password; + } + + /** + * Complete the login for a user by incrementing the logins and setting + * session data: user_id, username, roles. + * + * @param object user ORM object + * @return void + */ + protected function complete_login($user) + { + $user->complete_login(); + + return parent::complete_login($user); + } + + /** + * Compare password with original (hashed). Works for current (logged in) user + * + * @param string $password + * @return boolean + */ + public function check_password($password) + { + $user = $this->get_user(); + + if ($user === FALSE) + { + // nothing to compare + return FALSE; + } + + $hash = $this->hash_password($password, $this->find_salt($user->password)); + + return $hash == $user->password; + } + +} // End Auth ORM \ No newline at end of file diff --git a/includes/kohana/modules/auth/classes/model/auth/role.php b/includes/kohana/modules/auth/classes/model/auth/role.php new file mode 100644 index 00000000..042f1419 --- /dev/null +++ b/includes/kohana/modules/auth/classes/model/auth/role.php @@ -0,0 +1,27 @@ + array('through' => 'roles_users')); + + // Validation rules + protected $_rules = array( + 'name' => array( + 'not_empty' => NULL, + 'min_length' => array(4), + 'max_length' => array(32), + ), + 'description' => array( + 'max_length' => array(255), + ), + ); + +} // End Auth Role Model \ No newline at end of file diff --git a/includes/kohana/modules/auth/classes/model/auth/user.php b/includes/kohana/modules/auth/classes/model/auth/user.php new file mode 100644 index 00000000..179391ba --- /dev/null +++ b/includes/kohana/modules/auth/classes/model/auth/user.php @@ -0,0 +1,244 @@ + array('model' => 'user_token'), + 'roles' => array('model' => 'role', 'through' => 'roles_users'), + ); + + // Validation rules + protected $_rules = array( + 'username' => array( + 'not_empty' => NULL, + 'min_length' => array(4), + 'max_length' => array(32), + 'regex' => array('/^[-\pL\pN_.]++$/uD'), + ), + 'password' => array( + 'not_empty' => NULL, + 'min_length' => array(5), + 'max_length' => array(42), + ), + 'password_confirm' => array( + 'matches' => array('password'), + ), + 'email' => array( + 'not_empty' => NULL, + 'min_length' => array(4), + 'max_length' => array(127), + 'email' => NULL, + ), + ); + + // Validation callbacks + protected $_callbacks = array( + 'username' => array('username_available'), + 'email' => array('email_available'), + ); + + // Field labels + protected $_labels = array( + 'username' => 'username', + 'email' => 'email address', + 'password' => 'password', + 'password_confirm' => 'password confirmation', + ); + + // Columns to ignore + protected $_ignored_columns = array('password_confirm'); + + /** + * Validates login information from an array, and optionally redirects + * after a successful login. + * + * @param array values to check + * @param string URI or URL to redirect to + * @return boolean + */ + public function login(array & $array, $redirect = FALSE) + { + $fieldname = $this->unique_key($array['username']); + $array = Validate::factory($array) + ->label('username', $this->_labels[$fieldname]) + ->label('password', $this->_labels['password']) + ->filter(TRUE, 'trim') + ->rules('username', $this->_rules[$fieldname]) + ->rules('password', $this->_rules['password']); + + // Get the remember login option + $remember = isset($array['remember']); + + // Login starts out invalid + $status = FALSE; + + if ($array->check()) + { + // Attempt to load the user + $this->where($fieldname, '=', $array['username'])->find(); + + if ($this->loaded() AND Auth::instance()->login($this, $array['password'], $remember)) + { + if (is_string($redirect)) + { + // Redirect after a successful login + Request::instance()->redirect($redirect); + } + + // Login is successful + $status = TRUE; + } + else + { + $array->error('username', 'invalid'); + } + } + + return $status; + } + + /** + * Validates an array for a matching password and password_confirm field, + * and optionally redirects after a successful save. + * + * @param array values to check + * @param string URI or URL to redirect to + * @return boolean + */ + public function change_password(array & $array, $redirect = FALSE) + { + $array = Validate::factory($array) + ->label('password', $this->_labels['password']) + ->label('password_confirm', $this->_labels['password_confirm']) + ->filter(TRUE, 'trim') + ->rules('password', $this->_rules['password']) + ->rules('password_confirm', $this->_rules['password_confirm']); + + if ($status = $array->check()) + { + // Change the password + $this->password = $array['password']; + + if ($status = $this->save() AND is_string($redirect)) + { + // Redirect to the success page + Request::instance()->redirect($redirect); + } + } + + return $status; + } + + /** + * Complete the login for a user by incrementing the logins and saving login timestamp + * + * @return void + */ + public function complete_login() + { + if ( ! $this->_loaded) + { + // nothing to do + return; + } + + // Update the number of logins + $this->logins = new Database_Expression('logins + 1'); + + // Set the last login date + $this->last_login = time(); + + // Save the user + $this->save(); + } + + /** + * Does the reverse of unique_key_exists() by triggering error if username exists. + * Validation callback. + * + * @param Validate Validate object + * @param string field name + * @return void + */ + public function username_available(Validate $array, $field) + { + if ($this->unique_key_exists($array[$field], 'username')) + { + $array->error($field, 'username_available', array($array[$field])); + } + } + + /** + * Does the reverse of unique_key_exists() by triggering error if email exists. + * Validation callback. + * + * @param Validate Validate object + * @param string field name + * @return void + */ + public function email_available(Validate $array, $field) + { + if ($this->unique_key_exists($array[$field], 'email')) + { + $array->error($field, 'email_available', array($array[$field])); + } + } + + /** + * Tests if a unique key value exists in the database. + * + * @param mixed the value to test + * @param string field name + * @return boolean + */ + public function unique_key_exists($value, $field = NULL) + { + if ($field === NULL) + { + // Automatically determine field by looking at the value + $field = $this->unique_key($value); + } + + return (bool) DB::select(array('COUNT("*")', 'total_count')) + ->from($this->_table_name) + ->where($field, '=', $value) + ->where($this->_primary_key, '!=', $this->pk()) + ->execute($this->_db) + ->get('total_count'); + } + + /** + * Allows a model use both email and username as unique identifiers for login + * + * @param string unique value + * @return string field name + */ + public function unique_key($value) + { + return Validate::email($value) ? 'email' : 'username'; + } + + /** + * Saves the current object. Will hash password if it was changed. + * + * @return ORM + */ + public function save() + { + if (array_key_exists('password', $this->_changed)) + { + $this->_object['password'] = Auth::instance()->hash_password($this->_object['password']); + } + + return parent::save(); + } + +} // End Auth User Model \ No newline at end of file diff --git a/includes/kohana/modules/auth/classes/model/auth/user/token.php b/includes/kohana/modules/auth/classes/model/auth/user/token.php new file mode 100644 index 00000000..e7d820c0 --- /dev/null +++ b/includes/kohana/modules/auth/classes/model/auth/user/token.php @@ -0,0 +1,101 @@ + array()); + + // Current timestamp + protected $_now; + + /** + * Handles garbage collection and deleting of expired objects. + * + * @return void + */ + public function __construct($id = NULL) + { + parent::__construct($id); + + // Set the now, we use this a lot + $this->_now = time(); + + if (mt_rand(1, 100) === 1) + { + // Do garbage collection + $this->delete_expired(); + } + + if ($this->expires < $this->_now) + { + // This object has expired + $this->delete(); + } + } + + /** + * Overload saving to set the created time and to create a new token + * when the object is saved. + * + * @return ORM + */ + public function save() + { + if ($this->loaded() === FALSE) + { + // Set the created time, token, and hash of the user agent + $this->created = $this->_now; + $this->user_agent = sha1(Request::$user_agent); + } + + while (TRUE) + { + // Generate a new token + $this->token = $this->create_token(); + + try + { + return parent::save(); + } + catch (Kohana_Database_Exception $e) + { + // Collision occurred, token is not unique + } + } + } + + /** + * Deletes all expired tokens. + * + * @return ORM + */ + public function delete_expired() + { + // Delete all expired tokens + DB::delete($this->_table_name) + ->where('expires', '<', $this->_now) + ->execute($this->_db); + + return $this; + } + + /** + * Generate a new unique token. + * + * @return string + * @uses Text::random + */ + protected function create_token() + { + // Create a random token + return Text::random('alnum', 32); + } + +} // End Auth User Token Model \ No newline at end of file diff --git a/includes/kohana/modules/auth/classes/model/role.php b/includes/kohana/modules/auth/classes/model/role.php new file mode 100644 index 00000000..983fa96e --- /dev/null +++ b/includes/kohana/modules/auth/classes/model/role.php @@ -0,0 +1,7 @@ + 'ORM', + 'hash_method' => 'sha1', + 'salt_pattern' => '1, 3, 5, 9, 14, 15, 20, 21, 28, 30', + 'lifetime' => 1209600, + 'session_key' => 'auth_user', + + // Username/password combinations for the Auth File driver + 'users' => array( + // 'admin' => 'b3154acf3a344170077d11bdb5fff31532f679a1919e716a02', + ), + +); \ No newline at end of file diff --git a/includes/kohana/modules/auth/mysql.sql b/includes/kohana/modules/auth/mysql.sql new file mode 100644 index 00000000..1c58ac18 --- /dev/null +++ b/includes/kohana/modules/auth/mysql.sql @@ -0,0 +1,48 @@ +CREATE TABLE IF NOT EXISTS `roles` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `name` varchar(32) NOT NULL, + `description` varchar(255) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_name` (`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +INSERT INTO `roles` (`id`, `name`, `description`) VALUES(1, 'login', 'Login privileges, granted after account confirmation'); +INSERT INTO `roles` (`id`, `name`, `description`) VALUES(2, 'admin', 'Administrative user, has access to everything.'); + +CREATE TABLE IF NOT EXISTS `roles_users` ( + `user_id` int(10) UNSIGNED NOT NULL, + `role_id` int(10) UNSIGNED NOT NULL, + PRIMARY KEY (`user_id`,`role_id`), + KEY `fk_role_id` (`role_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `users` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `email` varchar(127) NOT NULL, + `username` varchar(32) NOT NULL DEFAULT '', + `password` char(50) NOT NULL, + `logins` int(10) UNSIGNED NOT NULL DEFAULT '0', + `last_login` int(10) UNSIGNED, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_username` (`username`), + UNIQUE KEY `uniq_email` (`email`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +CREATE TABLE IF NOT EXISTS `user_tokens` ( + `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT, + `user_id` int(11) UNSIGNED NOT NULL, + `user_agent` varchar(40) NOT NULL, + `token` varchar(32) NOT NULL, + `created` int(10) UNSIGNED NOT NULL, + `expires` int(10) UNSIGNED NOT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_token` (`token`), + KEY `fk_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +ALTER TABLE `roles_users` + ADD CONSTRAINT `roles_users_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + ADD CONSTRAINT `roles_users_ibfk_2` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE; + +ALTER TABLE `user_tokens` + ADD CONSTRAINT `user_tokens_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE; \ No newline at end of file diff --git a/includes/kohana/modules/auth/postgresql.sql b/includes/kohana/modules/auth/postgresql.sql new file mode 100644 index 00000000..9e192ba6 --- /dev/null +++ b/includes/kohana/modules/auth/postgresql.sql @@ -0,0 +1,53 @@ +CREATE TABLE roles +( + id serial, + "name" varchar(32) NOT NULL, + description text NOT NULL, + CONSTRAINT roles_id_pkey PRIMARY KEY (id), + CONSTRAINT roles_name_key UNIQUE (name) +); + +CREATE TABLE roles_users +( + user_id integer, + role_id integer +); + +CREATE TABLE users +( + id serial, + email varchar(318) NOT NULL, + username varchar(32) NOT NULL, + "password" varchar(50) NOT NULL, + logins integer NOT NULL DEFAULT 0, + last_login integer, + CONSTRAINT users_id_pkey PRIMARY KEY (id), + CONSTRAINT users_username_key UNIQUE (username), + CONSTRAINT users_email_key UNIQUE (email), + CONSTRAINT users_logins_check CHECK (logins >= 0) +); + +CREATE TABLE user_tokens +( + id serial, + user_id integer NOT NULL, + user_agent varchar(40) NOT NULL, + token character varying(32) NOT NULL, + created integer NOT NULL, + expires integer NOT NULL, + CONSTRAINT user_tokens_id_pkey PRIMARY KEY (id), + CONSTRAINT user_tokens_token_key UNIQUE (token) +); + +CREATE INDEX user_id_idx ON roles_users (user_id); +CREATE INDEX role_id_idx ON roles_users (role_id); + +ALTER TABLE roles_users + ADD CONSTRAINT user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + ADD CONSTRAINT role_id_fkey FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE; + +ALTER TABLE user_tokens + ADD CONSTRAINT user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + +INSERT INTO roles (name, description) VALUES ('login', 'Login privileges, granted after account confirmation'); +INSERT INTO roles (name, description) VALUES ('admin', 'Administrative user, has access to everything.'); \ No newline at end of file diff --git a/includes/kohana/modules/cache/README.md b/includes/kohana/modules/cache/README.md new file mode 100644 index 00000000..8c486cb7 --- /dev/null +++ b/includes/kohana/modules/cache/README.md @@ -0,0 +1,60 @@ +Kohana Cache library +==================== + +The cache library for Kohana 3 provides a simple interface to the most common cache solutions. Developers are free to add their own caching solutions that follow the cache design pattern defined within this module. + +Supported cache solutions +------------------------- + +Currently this module supports the following cache methods. + +1. APC +2. eAccelerator +3. Memcache +4. Memcached-tags (Supports tags) +5. SQLite (Supports tags) +6. File +7. Xcache + +Planned support +--------------- + +In the near future, additional support for the following methods will be included. + +1. Memcached + +Introduction to caching +----------------------- + +To use caching to the maximum potential, your application should be designed with caching in mind from the outset. In general, the most effective caches contain lots of small collections of data that are the result of expensive computational operations, such as searching through a large data set. + +There are many different caching methods available for PHP, from the very basic file based caching to opcode caching in eAccelerator and APC. Caching engines that use physical memory over disk based storage are always faster, however many do not support more advanced features such as tagging. + +Using Cache +----------- + +To use Kohana Cache, download and extract the latest stable release of Kohana Cache from [Github](http://github.com/samsoir/kohana-cache). Place the module into your Kohana instances modules folder. Finally enable the module within the application bootstrap within the section entitled _modules_. + +Quick example +------------- + +The following is a quick example of how to use Kohana Cache. The example is using the SQLite driver. + + 'bar', 'apples' => 'pear', 'BDFL' => 'Shadowhand'); + + // Save the data to cache, with an id of test_id and a lifetime of 10 minutes + $mycache->set('test_id', $data, 600); + + // Retrieve the data from cache + $retrieved_data = $mycache->get('test_id'); + + // Remove the cache item + $mycache->delete('test_id'); + + // Clear the cache of all stored items + $mycache->delete_all(); diff --git a/includes/kohana/modules/cache/classes/cache.php b/includes/kohana/modules/cache/classes/cache.php new file mode 100644 index 00000000..2b43c93b --- /dev/null +++ b/includes/kohana/modules/cache/classes/cache.php @@ -0,0 +1,3 @@ + array( // Default group + * 'driver' => 'memcache', // using Memcache driver + * 'servers' => array( // Available server definitions + * array( + * 'host' => 'localhost', + * 'port' => 11211, + * 'persistent' => FALSE + * ) + * ), + * 'compression' => FALSE, // Use compression? + * ), + * ) + * + * In cases where only one cache group is required, if the group is named `default` there is + * no need to pass the group name when instantiating a cache instance. + * + * #### General cache group configuration settings + * + * Below are the settings available to all types of cache driver. + * + * Name | Required | Description + * -------------- | -------- | --------------------------------------------------------------- + * driver | __YES__ | (_string_) The driver type to use + * + * Details of the settings specific to each driver are available within the drivers documentation. + * + * ### System requirements + * + * * Kohana 3.0.x + * * PHP 5.2.4 or greater + * + * @package Kohana + * @category Cache + * @version 2.0 + * @author Kohana Team + * @copyright (c) 2009-2010 Kohana Team + * @license http://kohanaphp.com/license + */ +abstract class Kohana_Cache { + + const DEFAULT_EXPIRE = 3600; + + /** + * @var string default driver to use + */ + public static $default = 'file'; + + /** + * @var Kohana_Cache instances + */ + public static $instances = array(); + + /** + * Creates a singleton of a Kohana Cache group. If no group is supplied + * the __default__ cache group is used. + * + * // Create an instance of the default group + * $default_group = Cache::instance(); + * + * // Create an instance of a group + * $foo_group = Cache::instance('foo'); + * + * // Access an instantiated group directly + * $foo_group = Cache::$instances['default']; + * + * @param string the name of the cache group to use [Optional] + * @return Kohana_Cache + * @throws Kohana_Cache_Exception + */ + public static function instance($group = NULL) + { + // If there is no group supplied + if ($group === NULL) + { + // Use the default setting + $group = Cache::$default; + } + + if (isset(Cache::$instances[$group])) + { + // Return the current group if initiated already + return Cache::$instances[$group]; + } + + $config = Kohana::config('cache'); + + if ( ! $config->offsetExists($group)) + { + throw new Kohana_Cache_Exception('Failed to load Kohana Cache group: :group', array(':group' => $group)); + } + + $config = $config->get($group); + + // Create a new cache type instance + $cache_class = 'Cache_'.ucfirst($config['driver']); + Cache::$instances[$group] = new $cache_class($config); + + // Return the instance + return Cache::$instances[$group]; + } + + /** + * @var Kohana_Config + */ + protected $_config; + + /** + * Ensures singleton pattern is observed, loads the default expiry + * + * @param array configuration + */ + protected function __construct(array $config) + { + $this->_config = $config; + } + + /** + * Overload the __clone() method to prevent cloning + * + * @return void + * @throws Kohana_Cache_Exception + */ + public function __clone() + { + throw new Kohana_Cache_Exception('Cloning of Kohana_Cache objects is forbidden'); + } + + /** + * Retrieve a cached value entry by id. + * + * // Retrieve cache entry from default group + * $data = Cache::instance()->get('foo'); + * + * // Retrieve cache entry from default group and return 'bar' if miss + * $data = Cache::instance()->get('foo', 'bar'); + * + * // Retrieve cache entry from memcache group + * $data = Cache::instance('memcache')->get('foo'); + * + * @param string id of cache to entry + * @param string default value to return if cache miss + * @return mixed + * @throws Kohana_Cache_Exception + */ + abstract public function get($id, $default = NULL); + + /** + * Set a value to cache with id and lifetime + * + * $data = 'bar'; + * + * // Set 'bar' to 'foo' in default group, using default expiry + * Cache::instance()->set('foo', $data); + * + * // Set 'bar' to 'foo' in default group for 30 seconds + * Cache::instance()->set('foo', $data, 30); + * + * // Set 'bar' to 'foo' in memcache group for 10 minutes + * if (Cache::instance('memcache')->set('foo', $data, 600)) + * { + * // Cache was set successfully + * return + * } + * + * @param string id of cache entry + * @param string data to set to cache + * @param integer lifetime in seconds + * @return boolean + */ + abstract public function set($id, $data, $lifetime = 3600); + + /** + * Delete a cache entry based on id + * + * // Delete 'foo' entry from the default group + * Cache::instance()->delete('foo'); + * + * // Delete 'foo' entry from the memcache group + * Cache::instance('memcache')->delete('foo') + * + * @param string id to remove from cache + * @return boolean + */ + abstract public function delete($id); + + /** + * Delete all cache entries. + * + * Beware of using this method when + * using shared memory cache systems, as it will wipe every + * entry within the system for all clients. + * + * // Delete all cache entries in the default group + * Cache::instance()->delete_all(); + * + * // Delete all cache entries in the memcache group + * Cache::instance('memcache')->delete_all(); + * + * @return boolean + */ + abstract public function delete_all(); + + /** + * Replaces troublesome characters with underscores. + * + * // Sanitize a cache id + * $id = $this->_sanitize_id($id); + * + * @param string id of cache to sanitize + * @return string + */ + protected function _sanitize_id($id) + { + // Change slashes and spaces to underscores + return str_replace(array('/', '\\', ' '), '_', $id); + } +} +// End Kohana_Cache \ No newline at end of file diff --git a/includes/kohana/modules/cache/classes/kohana/cache/apc.php b/includes/kohana/modules/cache/classes/kohana/cache/apc.php new file mode 100644 index 00000000..a6d33c84 --- /dev/null +++ b/includes/kohana/modules/cache/classes/kohana/cache/apc.php @@ -0,0 +1,133 @@ + array( // Driver group + * 'driver' => 'apc', // using APC driver + * ), + * ) + * + * In cases where only one cache group is required, if the group is named `default` there is + * no need to pass the group name when instantiating a cache instance. + * + * #### General cache group configuration settings + * + * Below are the settings available to all types of cache driver. + * + * Name | Required | Description + * -------------- | -------- | --------------------------------------------------------------- + * driver | __YES__ | (_string_) The driver type to use + * + * ### System requirements + * + * * Kohana 3.0.x + * * PHP 5.2.4 or greater + * * APC PHP extension + * + * @package Kohana + * @category Cache + * @author Kohana Team + * @copyright (c) 2009-2010 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_Cache_Apc extends Cache { + + /** + * Check for existence of the APC extension This method cannot be invoked externally. The driver must + * be instantiated using the `Cache::instance()` method. + * + * @param array configuration + * @throws Kohana_Cache_Exception + */ + protected function __construct(array $config) + { + if ( ! extension_loaded('apc')) + { + throw new Kohana_Cache_Exception('PHP APC extension is not available.'); + } + + parent::__construct($config); + } + + /** + * Retrieve a cached value entry by id. + * + * // Retrieve cache entry from apc group + * $data = Cache::instance('apc')->get('foo'); + * + * // Retrieve cache entry from apc group and return 'bar' if miss + * $data = Cache::instance('apc')->get('foo', 'bar'); + * + * @param string id of cache to entry + * @param string default value to return if cache miss + * @return mixed + * @throws Kohana_Cache_Exception + */ + public function get($id, $default = NULL) + { + return (($data = apc_fetch($this->_sanitize_id($id))) === FALSE) ? $default : $data; + } + + /** + * Set a value to cache with id and lifetime + * + * $data = 'bar'; + * + * // Set 'bar' to 'foo' in apc group, using default expiry + * Cache::instance('apc')->set('foo', $data); + * + * // Set 'bar' to 'foo' in apc group for 30 seconds + * Cache::instance('apc')->set('foo', $data, 30); + * + * @param string id of cache entry + * @param string data to set to cache + * @param integer lifetime in seconds + * @return boolean + */ + public function set($id, $data, $lifetime = NULL) + { + if ($lifetime === NULL) + { + $lifetime = Arr::get($this->_config, 'default_expire', Cache::DEFAULT_EXPIRE); + } + + return apc_store($this->_sanitize_id($id), $data, $lifetime); + } + + /** + * Delete a cache entry based on id + * + * // Delete 'foo' entry from the apc group + * Cache::instance('apc')->delete('foo'); + * + * @param string id to remove from cache + * @return boolean + */ + public function delete($id) + { + return apc_delete($this->_sanitize_id($id)); + } + + /** + * Delete all cache entries. + * + * Beware of using this method when + * using shared memory cache systems, as it will wipe every + * entry within the system for all clients. + * + * // Delete all cache entries in the apc group + * Cache::instance('apc')->delete_all(); + * + * @return boolean + */ + public function delete_all() + { + return apc_clear_cache('user'); + } +} \ No newline at end of file diff --git a/includes/kohana/modules/cache/classes/kohana/cache/eaccelerator.php b/includes/kohana/modules/cache/classes/kohana/cache/eaccelerator.php new file mode 100644 index 00000000..7abcd775 --- /dev/null +++ b/includes/kohana/modules/cache/classes/kohana/cache/eaccelerator.php @@ -0,0 +1,133 @@ + array( // Driver group + * 'driver' => 'eaccelerator', // using Eaccelerator driver + * ), + * ) + * + * In cases where only one cache group is required, if the group is named `default` there is + * no need to pass the group name when instantiating a cache instance. + * + * #### General cache group configuration settings + * + * Below are the settings available to all types of cache driver. + * + * Name | Required | Description + * -------------- | -------- | --------------------------------------------------------------- + * driver | __YES__ | (_string_) The driver type to use + * + * ### System requirements + * + * * Kohana 3.0.x + * * PHP 5.2.4 or greater + * * Eaccelerator PHP extension + * + * @package Kohana + * @category Cache + * @author Kohana Team + * @copyright (c) 2009-2010 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_Cache_Eaccelerator extends Cache { + + /** + * Check for existence of the eAccelerator extension This method cannot be invoked externally. The driver must + * be instantiated using the `Cache::instance()` method. + * + * @param array configuration + * @throws Kohana_Cache_Exception + */ + protected function __construct(array $config) + { + if ( ! extension_loaded('eaccelerator')) + { + throw new Kohana_Cache_Exception('PHP eAccelerator extension is not available.'); + } + + parent::__construct($config); + } + + /** + * Retrieve a cached value entry by id. + * + * // Retrieve cache entry from eaccelerator group + * $data = Cache::instance('eaccelerator')->get('foo'); + * + * // Retrieve cache entry from eaccelerator group and return 'bar' if miss + * $data = Cache::instance('eaccelerator')->get('foo', 'bar'); + * + * @param string id of cache to entry + * @param string default value to return if cache miss + * @return mixed + * @throws Kohana_Cache_Exception + */ + public function get($id, $default = NULL) + { + return (($data = eaccelerator_get($this->_sanitize_id($id))) === FALSE) ? $default : $data; + } + + /** + * Set a value to cache with id and lifetime + * + * $data = 'bar'; + * + * // Set 'bar' to 'foo' in eaccelerator group, using default expiry + * Cache::instance('eaccelerator')->set('foo', $data); + * + * // Set 'bar' to 'foo' in eaccelerator group for 30 seconds + * Cache::instance('eaccelerator')->set('foo', $data, 30); + * + * @param string id of cache entry + * @param string data to set to cache + * @param integer lifetime in seconds + * @return boolean + */ + public function set($id, $data, $lifetime = NULL) + { + if ($lifetime === NULL) + { + $lifetime = time() + Arr::get($this->_config, 'default_expire', Cache::DEFAULT_EXPIRE); + } + + return eaccelerator_put($this->_sanitize_id($id), $data, $lifetime); + } + + /** + * Delete a cache entry based on id + * + * // Delete 'foo' entry from the eaccelerator group + * Cache::instance('eaccelerator')->delete('foo'); + * + * @param string id to remove from cache + * @return boolean + */ + public function delete($id) + { + return eaccelerator_rm($this->_sanitize_id($id)); + } + + /** + * Delete all cache entries. + * + * Beware of using this method when + * using shared memory cache systems, as it will wipe every + * entry within the system for all clients. + * + * // Delete all cache entries in the eaccelerator group + * Cache::instance('eaccelerator')->delete_all(); + * + * @return boolean + */ + public function delete_all() + { + return eaccelerator_clean(); + } +} \ No newline at end of file diff --git a/includes/kohana/modules/cache/classes/kohana/cache/exception.php b/includes/kohana/modules/cache/classes/kohana/cache/exception.php new file mode 100644 index 00000000..f8677e78 --- /dev/null +++ b/includes/kohana/modules/cache/classes/kohana/cache/exception.php @@ -0,0 +1,3 @@ + array( // File driver group + * 'driver' => 'file', // using File driver + * 'cache_dir' => APPPATH.'cache/.kohana_cache', // Cache location + * ), + * ) + * + * In cases where only one cache group is required, if the group is named `default` there is + * no need to pass the group name when instantiating a cache instance. + * + * #### General cache group configuration settings + * + * Below are the settings available to all types of cache driver. + * + * Name | Required | Description + * -------------- | -------- | --------------------------------------------------------------- + * driver | __YES__ | (_string_) The driver type to use + * cache_dir | __NO__ | (_string_) The cache directory to use for this cache instance + * + * ### System requirements + * + * * Kohana 3.0.x + * * PHP 5.2.4 or greater + * + * @package Kohana + * @category Cache + * @author Kohana Team + * @copyright (c) 2009-2010 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_Cache_File extends Cache implements Kohana_Cache_GarbageCollect { + + // !!! NOTICE !!! + // THIS CONSTANT IS USED BY THE FILE CACHE CLASS + // INTERNALLY. USE THE CONFIGURATION FILE TO + // REDEFINE THE CACHE DIRECTORY. + const CACHE_DIR = 'cache/.kohana_cache'; + + /** + * Creates a hashed filename based on the string. This is used + * to create shorter unique IDs for each cache filename. + * + * // Create the cache filename + * $filename = Cache_File::filename($this->_sanitize_id($id)); + * + * @param string string to hash into filename + * @return string + */ + protected static function filename($string) + { + return sha1($string).'.json'; + } + + /** + * @var string the caching directory + */ + protected $_cache_dir; + + /** + * Constructs the file cache driver. This method cannot be invoked externally. The file cache driver must + * be instantiated using the `Cache::instance()` method. + * + * @param array config + * @throws Kohana_Cache_Exception + */ + protected function __construct(array $config) + { + // Setup parent + parent::__construct($config); + + try + { + $directory = Arr::get($this->_config, 'cache_dir', APPPATH.Cache_File::CACHE_DIR); + $this->_cache_dir = new RecursiveDirectoryIterator($directory); + } + catch (UnexpectedValueException $e) + { + if ( ! mkdir($directory, 0777, TRUE)) + { + throw new Kohana_Cache_Exception('Failed to create the defined cache directory : :directory', array(':directory' => $directory)); + } + chmod($directory, 0777); + $this->_cache_dir = new RecursiveDirectoryIterator($directory); + } + + // If the defined directory is a file, get outta here + if ($this->_cache_dir->isFile()) + { + throw new Kohana_Cache_Exception('Unable to create cache directory as a file already exists : :resource', array(':resource' => $this->_cache_dir->getRealPath())); + } + + // Check the read status of the directory + if ( ! $this->_cache_dir->isReadable()) + { + throw new Kohana_Cache_Exception('Unable to read from the cache directory :resource', array(':resource' => $this->_cache_dir->getRealPath())); + } + + // Check the write status of the directory + if ( ! $this->_cache_dir->isWritable()) + { + throw new Kohana_Cache_Exception('Unable to write to the cache directory :resource', array(':resource' => $this->_cache_dir->getRealPath())); + } + } + + /** + * Retrieve a cached value entry by id. + * + * // Retrieve cache entry from file group + * $data = Cache::instance('file')->get('foo'); + * + * // Retrieve cache entry from file group and return 'bar' if miss + * $data = Cache::instance('file')->get('foo', 'bar'); + * + * @param string id of cache to entry + * @param string default value to return if cache miss + * @return mixed + * @throws Kohana_Cache_Exception + */ + public function get($id, $default = NULL) + { + $filename = Cache_File::filename($this->_sanitize_id($id)); + $directory = $this->_resolve_directory($filename); + + // Wrap operations in try/catch to handle notices + try + { + // Open file + $file = new SplFileInfo($directory.$filename); + + // If file does not exist + if ( ! $file->getRealPath()) + { + // Return default value + return $default; + } + else + { + // Open the file and extract the json + $json = $file->openFile()->current(); + + // Decode the json into PHP object + $data = json_decode($json); + + // Test the expiry + if ($data->expiry < time()) + { + // Delete the file + $this->_delete_file($file, NULL, TRUE); + + // Return default value + return $default; + } + else + { + return ($data->type === 'string') ? $data->payload : unserialize($data->payload); + } + } + + } + catch (ErrorException $e) + { + // Handle ErrorException caused by failed unserialization + if ($e->getCode() === E_NOTICE) + { + throw new Kohana_Cache_Exception(__METHOD__.' failed to unserialize cached object with message : '.$e->getMessage()); + } + + // Otherwise throw the exception + throw $e; + } + } + + /** + * Set a value to cache with id and lifetime + * + * $data = 'bar'; + * + * // Set 'bar' to 'foo' in file group, using default expiry + * Cache::instance('file')->set('foo', $data); + * + * // Set 'bar' to 'foo' in file group for 30 seconds + * Cache::instance('file')->set('foo', $data, 30); + * + * @param string id of cache entry + * @param string data to set to cache + * @param integer lifetime in seconds + * @return boolean + */ + public function set($id, $data, $lifetime = NULL) + { + $filename = Cache_File::filename($this->_sanitize_id($id)); + $directory = $this->_resolve_directory($filename); + + // If lifetime is NULL + if ($lifetime === NULL) + { + // Set to the default expiry + $lifetime = Arr::get($this->_config, 'default_expire', Cache::DEFAULT_EXPIRE); + } + + // Open directory + $dir = new SplFileInfo($directory); + + // If the directory path is not a directory + if ( ! $dir->isDir()) + { + // Create the directory + if ( ! mkdir($directory, 0777, TRUE)) + { + throw new Kohana_Cache_Exception(__METHOD__.' unable to create directory : :directory', array(':directory' => $directory)); + } + + // chmod to solve potential umask issues + chmod($directory, 0777); + } + + // Open file to inspect + $resouce = new SplFileInfo($directory.$filename); + $file = $resouce->openFile('w'); + + try + { + $type = gettype($data); + + // Serialize the data + $data = json_encode((object) array( + 'payload' => ($type === 'string') ? $data : serialize($data), + 'expiry' => time() + $lifetime, + 'type' => $type + )); + + $size = strlen($data); + } + catch (ErrorException $e) + { + // If serialize through an error exception + if ($e->getCode() === E_NOTICE) + { + // Throw a caching error + throw new Kohana_Cache_Exception(__METHOD__.' failed to serialize data for caching with message : '.$e->getMessage()); + } + + // Else rethrow the error exception + throw $e; + } + + try + { + $file->fwrite($data, $size); + return (bool) $file->fflush(); + } + catch (Exception $e) + { + throw $e; + } + } + + /** + * Delete a cache entry based on id + * + * // Delete 'foo' entry from the file group + * Cache::instance('file')->delete('foo'); + * + * @param string id to remove from cache + * @return boolean + */ + public function delete($id) + { + $filename = Cache_File::filename($this->_sanitize_id($id)); + $directory = $this->_resolve_directory($filename); + + return $this->_delete_file(new SplFileInfo($directory.$filename), NULL, TRUE); + } + + /** + * Delete all cache entries. + * + * Beware of using this method when + * using shared memory cache systems, as it will wipe every + * entry within the system for all clients. + * + * // Delete all cache entries in the file group + * Cache::instance('file')->delete_all(); + * + * @return boolean + */ + public function delete_all() + { + return $this->_delete_file($this->_cache_dir, TRUE); + } + + /** + * Garbage collection method that cleans any expired + * cache entries from the cache. + * + * @return void + */ + public function garbage_collect() + { + $this->_delete_file($this->_cache_dir, TRUE, FALSE, TRUE); + return; + } + + /** + * Deletes files recursively and returns FALSE on any errors + * + * // Delete a file or folder whilst retaining parent directory and ignore all errors + * $this->_delete_file($folder, TRUE, TRUE); + * + * @param SplFileInfo file + * @param boolean retain the parent directory + * @param boolean ignore_errors to prevent all exceptions interrupting exec + * @param boolean only expired files + * @return boolean + * @throws Kohana_Cache_Exception + */ + protected function _delete_file(SplFileInfo $file, $retain_parent_directory = FALSE, $ignore_errors = FALSE, $only_expired = FALSE) + { + // Allow graceful error handling + try + { + // If is file + if ($file->isFile()) + { + try + { + // If only expired is not set + if ($only_expired === FALSE) + { + // We want to delete the file + $delete = TRUE; + } + // Otherwise... + else + { + // Assess the file expiry to flag it for deletion + $json = $file->openFile('r')->current(); + $data = json_decode($json); + $delete = $data->expiry < time(); + } + + // If the delete flag is set + if ($delete === TRUE) + { + // Try to delete + unlink($file->getRealPath()); + } + } + catch (ErrorException $e) + { + // Catch any delete file warnings + if ($e->getCode() === E_WARNING) + { + throw new Kohana_Cache_Exception(__METHOD__.' failed to delete file : :file', array(':file' => $file->getRealPath())); + } + } + } + // Else, is directory + else if ($file->isDir()) + { + // Create new DirectoryIterator + $files = new DirectoryIterator($file->getPathname()); + + // Iterate over each entry + while ($files->valid()) + { + // Extract the entry name + $name = $files->getFilename(); + + // If the name is not a dot + if ($name != '.' and $name != '..') + { + // Create new file resource + $fp = new SplFileInfo($files->getRealPath()); + // Delete the file + $this->_delete_file($fp); + } + + // Move the file pointer on + $files->next(); + } + + // If set to retain parent directory, return now + if ($retain_parent_directory) + { + return TRUE; + } + + try + { + // Remove the files iterator + // (fixes Windows PHP which has permission issues with open iterators) + unset($files); + + // Try to remove the parent directory + return rmdir($file->getRealPath()); + } + catch (ErrorException $e) + { + // Catch any delete directory warnings + if ($e->getCode() === E_WARNING) + { + throw new Kohana_Cache_Exception(__METHOD__.' failed to delete directory : :directory', array(':directory' => $file->getRealPath())); + } + } + } + } + // Catch all exceptions + catch (Exception $e) + { + // If ignore_errors is on + if ($ignore_errors === TRUE) + { + // Return + return FALSE; + } + // Throw exception + throw $e; + } + } + + /** + * Resolves the cache directory real path from the filename + * + * // Get the realpath of the cache folder + * $realpath = $this->_resolve_directory($filename); + * + * @param string filename to resolve + * @return string + */ + protected function _resolve_directory($filename) + { + return $this->_cache_dir->getRealPath().DIRECTORY_SEPARATOR.$filename[0].$filename[1].DIRECTORY_SEPARATOR; + } +} diff --git a/includes/kohana/modules/cache/classes/kohana/cache/garbagecollect.php b/includes/kohana/modules/cache/classes/kohana/cache/garbagecollect.php new file mode 100644 index 00000000..228dd610 --- /dev/null +++ b/includes/kohana/modules/cache/classes/kohana/cache/garbagecollect.php @@ -0,0 +1,23 @@ + array( // Default group + * 'driver' => 'memcache', // using Memcache driver + * 'servers' => array( // Available server definitions + * // First memcache server server + * array( + * 'host' => 'localhost', + * 'port' => 11211, + * 'persistent' => FALSE + * 'weight' => 1, + * 'timeout' => 1, + * 'retry_interval' => 15, + * 'status' => TRUE, + * 'failure_callback' => array('className', 'classMethod') + * ), + * // Second memcache server + * array( + * 'host' => '192.168.1.5', + * 'port' => 22122, + * 'persistent' => TRUE + * ) + * ), + * 'compression' => FALSE, // Use compression? + * ), + * ) + * + * In cases where only one cache group is required, if the group is named `default` there is + * no need to pass the group name when instantiating a cache instance. + * + * #### General cache group configuration settings + * + * Below are the settings available to all types of cache driver. + * + * Name | Required | Description + * -------------- | -------- | --------------------------------------------------------------- + * driver | __YES__ | (_string_) The driver type to use + * servers | __YES__ | (_array_) Associative array of server details, must include a __host__ key. (see _Memcache server configuration_ below) + * compression | __NO__ | (_boolean_) Use data compression when caching + * + * #### Memcache server configuration + * + * The following settings should be used when defining each memcache server + * + * Name | Required | Description + * ---------------- | -------- | --------------------------------------------------------------- + * host | __YES__ | (_string_) The host of the memcache server, i.e. __localhost__; or __127.0.0.1__; or __memcache.domain.tld__ + * port | __NO__ | (_integer_) Point to the port where memcached is listening for connections. Set this parameter to 0 when using UNIX domain sockets. Default __11211__ + * persistent | __NO__ | (_boolean_) Controls the use of a persistent connection. Default __TRUE__ + * weight | __NO__ | (_integer_) Number of buckets to create for this server which in turn control its probability of it being selected. The probability is relative to the total weight of all servers. Default __1__ + * timeout | __NO__ | (_integer_) Value in seconds which will be used for connecting to the daemon. Think twice before changing the default value of 1 second - you can lose all the advantages of caching if your connection is too slow. Default __1__ + * retry_interval | __NO__ | (_integer_) Controls how often a failed server will be retried, the default value is 15 seconds. Setting this parameter to -1 disables automatic retry. Default __15__ + * status | __NO__ | (_boolean_) Controls if the server should be flagged as online. Default __TRUE__ + * failure_callback | __NO__ | (_[callback](http://www.php.net/manual/en/language.pseudo-types.php#language.types.callback)_) Allows the user to specify a callback function to run upon encountering an error. The callback is run before failover is attempted. The function takes two parameters, the hostname and port of the failed server. Default __NULL__ + * + * ### System requirements + * + * * Kohana 3.0.x + * * PHP 5.2.4 or greater + * * Memcache (plus Memcached-tags for native tagging support) + * * Zlib + * + * @package Kohana + * @category Cache + * @version 2.0 + * @author Kohana Team + * @copyright (c) 2009-2010 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_Cache_Memcache extends Cache { + + // Memcache has a maximum cache lifetime of 30 days + const CACHE_CEILING = 2592000; + + /** + * Memcache resource + * + * @var Memcache + */ + protected $_memcache; + + /** + * Flags to use when storing values + * + * @var string + */ + protected $_flags; + + /** + * Constructs the memcache Kohana_Cache object + * + * @param array configuration + * @throws Kohana_Cache_Exception + */ + protected function __construct(array $config) + { + // Check for the memcache extention + if ( ! extension_loaded('memcache')) + { + throw new Kohana_Cache_Exception('Memcache PHP extention not loaded'); + } + + parent::__construct($config); + + // Setup Memcache + $this->_memcache = new Memcache; + + // Load servers from configuration + $servers = Arr::get($this->_config, 'servers', NULL); + + if ( ! $servers) + { + // Throw an exception if no server found + throw new Kohana_Cache_Exception('No Memcache servers defined in configuration'); + } + + // Setup default server configuration + $config = array( + 'host' => 'localhost', + 'port' => 11211, + 'persistent' => FALSE, + 'weight' => 1, + 'timeout' => 1, + 'retry_interval' => 15, + 'status' => TRUE, + 'failure_callback' => array($this, '_failed_request'), + ); + + // Add the memcache servers to the pool + foreach ($servers as $server) + { + // Merge the defined config with defaults + $server += $config; + + if ( ! $this->_memcache->addServer($server['host'], $server['port'], $server['persistent'], $server['weight'], $server['timeout'], $server['retry_interval'], $server['status'], $server['failure_callback'])) + { + throw new Kohana_Cache_Exception('Memcache could not connect to host \':host\' using port \':port\'', array(':host' => $server['host'], ':port' => $server['port'])); + } + } + + // Setup the flags + $this->_flags = Arr::get($this->_config, 'compression', FALSE) ? MEMCACHE_COMPRESSED : FALSE; + } + + /** + * Retrieve a cached value entry by id. + * + * // Retrieve cache entry from memcache group + * $data = Cache::instance('memcache')->get('foo'); + * + * // Retrieve cache entry from memcache group and return 'bar' if miss + * $data = Cache::instance('memcache')->get('foo', 'bar'); + * + * @param string id of cache to entry + * @param string default value to return if cache miss + * @return mixed + * @throws Kohana_Cache_Exception + */ + public function get($id, $default = NULL) + { + // Get the value from Memcache + $value = $this->_memcache->get($this->_sanitize_id($id)); + + // If the value wasn't found, normalise it + if ($value === FALSE) + { + $value = (NULL === $default) ? NULL : $default; + } + + // Return the value + return $value; + } + + /** + * Set a value to cache with id and lifetime + * + * $data = 'bar'; + * + * // Set 'bar' to 'foo' in memcache group for 10 minutes + * if (Cache::instance('memcache')->set('foo', $data, 600)) + * { + * // Cache was set successfully + * return + * } + * + * @param string id of cache entry + * @param mixed data to set to cache + * @param integer lifetime in seconds, maximum value 2592000 + * @return boolean + */ + public function set($id, $data, $lifetime = 3600) + { + // If the lifetime is greater than the ceiling + if ($lifetime > Cache_Memcache::CACHE_CEILING) + { + // Set the lifetime to maximum cache time + $lifetime = Cache_Memcache::CACHE_CEILING + time(); + } + // Else if the lifetime is greater than zero + elseif ($lifetime > 0) + { + $lifetime += time(); + } + // Else + else + { + // Normalise the lifetime + $lifetime = 0; + } + + // Set the data to memcache + return $this->_memcache->set($this->_sanitize_id($id), $data, $this->_flags, $lifetime); + } + + /** + * Delete a cache entry based on id + * + * // Delete the 'foo' cache entry immediately + * Cache::instance('memcache')->delete('foo'); + * + * // Delete the 'bar' cache entry after 30 seconds + * Cache::instance('memcache')->delete('bar', 30); + * + * @param string id of entry to delete + * @param integer timeout of entry, if zero item is deleted immediately, otherwise the item will delete after the specified value in seconds + * @return boolean + */ + public function delete($id, $timeout = 0) + { + // Delete the id + return $this->_memcache->delete($this->_sanitize_id($id), $timeout); + } + + /** + * Delete all cache entries. + * + * Beware of using this method when + * using shared memory cache systems, as it will wipe every + * entry within the system for all clients. + * + * // Delete all cache entries in the default group + * Cache::instance('memcache')->delete_all(); + * + * @return boolean + */ + public function delete_all() + { + $result = $this->_memcache->flush(); + + // We must sleep after flushing, or overwriting will not work! + // @see http://php.net/manual/en/function.memcache-flush.php#81420 + sleep(1); + + return $result; + } + + /** + * Callback method for Memcache::failure_callback to use if any Memcache call + * on a particular server fails. This method switches off that instance of the + * server if the configuration setting `instant_death` is set to `TRUE`. + * + * @param string hostname + * @param integer port + * @return void|boolean + * @since 3.0.8 + */ + protected function _failed_request($hostname, $port) + { + if ( ! $this->_config['instant_death']) + return; + + // Setup non-existent host + $host = FALSE; + + // Get host settings from configuration + foreach ($this->_config['servers'] as $server) + { + if ($hostname == $server['host'] and $port == $server['port']) + { + $host = $server; + continue; + } + } + + if ( ! $host) + return; + else + { + return $this->_memcache->setServerParams( + $host['host'], + $host['port'], + $host['timeout'], + $host['retry_interval'], + FALSE, + array($this, '_failed_request' + )); + } + } +} \ No newline at end of file diff --git a/includes/kohana/modules/cache/classes/kohana/cache/memcachetag.php b/includes/kohana/modules/cache/classes/kohana/cache/memcachetag.php new file mode 100644 index 00000000..cdb0db73 --- /dev/null +++ b/includes/kohana/modules/cache/classes/kohana/cache/memcachetag.php @@ -0,0 +1,76 @@ +_memcache, 'tag_add')) + { + throw new Kohana_Cache_Exception('Memcached-tags PHP plugin not present. Please see http://code.google.com/p/memcached-tags/ for more information'); + } + + parent::__construct($config); + } + + /** + * Set a value based on an id with tags + * + * @param string id + * @param mixed data + * @param integer lifetime [Optional] + * @param array tags [Optional] + * @return boolean + */ + public function set_with_tags($id, $data, $lifetime = NULL, array $tags = NULL) + { + $result = $this->set($id, $data, $lifetime); + + if ($result and $tags) + { + foreach ($tags as $tag) + { + $this->_memcache->tag_add($tag, $id); + } + } + + return $result; + } + + /** + * Delete cache entries based on a tag + * + * @param string tag + * @return boolean + */ + public function delete_tag($tag) + { + return $this->_memcache->tag_delete($tag); + } + + /** + * Find cache entries based on a tag + * + * @param string tag + * @return void + * @throws Kohana_Cache_Exception + */ + public function find($tag) + { + throw new Kohana_Cache_Exception('Memcached-tags does not support finding by tag'); + } +} \ No newline at end of file diff --git a/includes/kohana/modules/cache/classes/kohana/cache/sqlite.php b/includes/kohana/modules/cache/classes/kohana/cache/sqlite.php new file mode 100644 index 00000000..32619771 --- /dev/null +++ b/includes/kohana/modules/cache/classes/kohana/cache/sqlite.php @@ -0,0 +1,336 @@ +_config, 'database', NULL); + + if ($database === NULL) + { + throw new Kohana_Cache_Exception('Database path not available in Kohana Cache configuration'); + } + + // Load new Sqlite DB + $this->_db = new PDO('sqlite:'.$database); + + // Test for existing DB + $result = $this->_db->query("SELECT * FROM sqlite_master WHERE name = 'caches' AND type = 'table'")->fetchAll(); + + // If there is no table, create a new one + if (0 == count($result)) + { + $database_schema = Arr::get($this->_config, 'schema', NULL); + + if ($database_schema === NULL) + { + throw new Kohana_Cache_Exception('Database schema not found in Kohana Cache configuration'); + } + + try + { + // Create the caches table + $this->_db->query(Arr::get($this->_config, 'schema', NULL)); + } + catch (PDOException $e) + { + throw new Kohana_Cache_Exception('Failed to create new SQLite caches table with the following error : :error', array(':error' => $e->getMessage())); + } + } + } + + /** + * Retrieve a value based on an id + * + * @param string id + * @param string default [Optional] Default value to return if id not found + * @return mixed + * @throws Kohana_Cache_Exception + */ + public function get($id, $default = NULL) + { + // Prepare statement + $statement = $this->_db->prepare('SELECT id, expiration, cache FROM caches WHERE id = :id LIMIT 0, 1'); + + // Try and load the cache based on id + try + { + $statement->execute(array(':id' => $this->_sanitize_id($id))); + } + catch (PDOException $e) + { + throw new Kohana_Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); + } + + if ( ! $result = $statement->fetch(PDO::FETCH_OBJ)) + { + return $default; + } + + // If the cache has expired + if ($result->expiration != 0 and $result->expiration <= time()) + { + // Delete it and return default value + $this->delete($id); + return $default; + } + // Otherwise return cached object + else + { + // Disable notices for unserializing + $ER = error_reporting(~E_NOTICE); + + // Return the valid cache data + $data = unserialize($result->cache); + + // Turn notices back on + error_reporting($ER); + + // Return the resulting data + return $data; + } + } + + /** + * Set a value based on an id. Optionally add tags. + * + * @param string id + * @param mixed data + * @param integer lifetime [Optional] + * @return boolean + */ + public function set($id, $data, $lifetime = NULL) + { + return (bool) $this->set_with_tags($id, $data, $lifetime); + } + + /** + * Delete a cache entry based on id + * + * @param string id + * @param integer timeout [Optional] + * @return boolean + * @throws Kohana_Cache_Exception + */ + public function delete($id) + { + // Prepare statement + $statement = $this->_db->prepare('DELETE FROM caches WHERE id = :id'); + + // Remove the entry + try + { + $statement->execute(array(':id' => $this->_sanitize_id($id))); + } + catch (PDOException $e) + { + throw new Kohana_Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); + } + + return (bool) $statement->rowCount(); + } + + /** + * Delete all cache entries + * + * @return boolean + */ + public function delete_all() + { + // Prepare statement + $statement = $this->_db->prepare('DELETE FROM caches'); + + // Remove the entry + try + { + $statement->execute(); + } + catch (PDOException $e) + { + throw new Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); + } + + return (bool) $statement->rowCount(); + } + + /** + * Set a value based on an id. Optionally add tags. + * + * @param string id + * @param mixed data + * @param integer lifetime [Optional] + * @param array tags [Optional] + * @return boolean + * @throws Kohana_Cache_Exception + */ + public function set_with_tags($id, $data, $lifetime = NULL, array $tags = NULL) + { + // Serialize the data + $data = serialize($data); + + // Normalise tags + $tags = (NULL === $tags) ? NULL : '<'.implode('>,<', $tags).'>'; + + // Setup lifetime + if ($lifetime === NULL) + { + $lifetime = (0 === Arr::get('default_expire', NULL)) ? 0 : Arr::get($this->_config, 'default_expire', Cache::DEFAULT_EXPIRE) + time(); + } + else + { + $lifetime = (0 === $lifetime) ? 0 : $lifetime + time(); + } + + // Prepare statement + // $this->exists() may throw Kohana_Cache_Exception, no need to catch/rethrow + $statement = $this->exists($id) ? $this->_db->prepare('UPDATE caches SET expiration = :expiration, cache = :cache, tags = :tags WHERE id = :id') : $this->_db->prepare('INSERT INTO caches (id, cache, expiration, tags) VALUES (:id, :cache, :expiration, :tags)'); + + // Try to insert + try + { + $statement->execute(array(':id' => $this->_sanitize_id($id), ':cache' => $data, ':expiration' => $lifetime, ':tags' => $tags)); + } + catch (PDOException $e) + { + throw new Kohana_Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); + } + + return (bool) $statement->rowCount(); + } + + /** + * Delete cache entries based on a tag + * + * @param string tag + * @param integer timeout [Optional] + * @return boolean + * @throws Kohana_Cache_Exception + */ + public function delete_tag($tag) + { + // Prepare the statement + $statement = $this->_db->prepare('DELETE FROM caches WHERE tags LIKE :tag'); + + // Try to delete + try + { + $statement->execute(array(':tag' => "%<{$tag}>%")); + } + catch (PDOException $e) + { + throw new Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); + } + + return (bool) $statement->rowCount(); + } + + /** + * Find cache entries based on a tag + * + * @param string tag + * @return array + * @throws Kohana_Cache_Exception + */ + public function find($tag) + { + // Prepare the statement + $statement = $this->_db->prepare('SELECT id, cache FROM caches WHERE tags LIKE :tag'); + + // Try to find + try + { + if ( ! $statement->execute(array(':tag' => "%<{$tag}>%"))) + { + return array(); + } + } + catch (PDOException $e) + { + throw new Kohana_Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); + } + + $result = array(); + + while ($row = $statement->fetchObject()) + { + // Disable notices for unserializing + $ER = error_reporting(~E_NOTICE); + + $result[$row->id] = unserialize($row->cache); + + // Turn notices back on + error_reporting($ER); + } + + return $result; + } + + /** + * Garbage collection method that cleans any expired + * cache entries from the cache. + * + * @return void + */ + public function garbage_collect() + { + // Create the sequel statement + $statement = $this->_db->prepare('DELETE FROM caches WHERE expiration < :expiration'); + + try + { + $statement->execute(array(':expiration' => time())); + } + catch (PDOException $e) + { + throw new Kohana_Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); + } + } + + /** + * Tests whether an id exists or not + * + * @param string id + * @return boolean + * @throws Kohana_Cache_Exception + */ + protected function exists($id) + { + $statement = $this->_db->prepare('SELECT id FROM caches WHERE id = :id'); + try + { + $statement->execute(array(':id' => $this->_sanitize_id($id))); + } + catch (PDOExeption $e) + { + throw new Kohana_Cache_Exception('There was a problem querying the local SQLite3 cache. :error', array(':error' => $e->getMessage())); + } + + return (bool) $statement->fetchAll(); + } +} \ No newline at end of file diff --git a/includes/kohana/modules/cache/classes/kohana/cache/tagging.php b/includes/kohana/modules/cache/classes/kohana/cache/tagging.php new file mode 100644 index 00000000..b7fe1ab7 --- /dev/null +++ b/includes/kohana/modules/cache/classes/kohana/cache/tagging.php @@ -0,0 +1,42 @@ +_sanitize_id($id))) === NULL) ? $default : $data; + } + + /** + * Set a value based on an id. Optionally add tags. + * + * @param string id + * @param string data + * @param integer lifetime [Optional] + * @return boolean + */ + public function set($id, $data, $lifetime = NULL) + { + if (NULL === $lifetime) + { + $lifetime = Arr::get($this->_config, 'default_expire', Cache::DEFAULT_EXPIRE); + } + + return xcache_set($this->_sanitize_id($id), $data, $lifetime); + } + + /** + * Delete a cache entry based on id + * + * @param string id + * @param integer timeout [Optional] + * @return boolean + */ + public function delete($id) + { + return xcache_unset($this->_sanitize_id($id)); + } + + /** + * Delete all cache entries + * To use this method xcache.admin.enable_auth has to be Off in xcache.ini + * + * @return void + */ + public function delete_all() + { + xcache_clear_cache(XC_TYPE_PHP, 0); + } +} diff --git a/includes/kohana/modules/cache/config/cache.php b/includes/kohana/modules/cache/config/cache.php new file mode 100644 index 00000000..c509c704 --- /dev/null +++ b/includes/kohana/modules/cache/config/cache.php @@ -0,0 +1,71 @@ + array + ( + 'driver' => 'memcache', + 'default_expire' => 3600, + 'compression' => FALSE, // Use Zlib compression (can cause issues with integers) + 'servers' => array + ( + array + ( + 'host' => 'localhost', // Memcache Server + 'port' => 11211, // Memcache port number + 'persistent' => FALSE, // Persistent connection + 'weight' => 1, + 'timeout' => 1, + 'retry_interval' => 15, + 'status' => TRUE, + ), + ), + 'instant_death' => TRUE, // Take server offline immediately on first fail (no retry) + ), + 'memcachetag' => array + ( + 'driver' => 'memcachetag', + 'default_expire' => 3600, + 'compression' => FALSE, // Use Zlib compression (can cause issues with integers) + 'servers' => array + ( + array + ( + 'host' => 'localhost', // Memcache Server + 'port' => 11211, // Memcache port number + 'persistent' => FALSE, // Persistent connection + 'weight' => 1, + 'timeout' => 1, + 'retry_interval' => 15, + 'status' => TRUE, + ), + ), + 'instant_death' => TRUE, + ), + 'apc' => array + ( + 'driver' => 'apc', + 'default_expire' => 3600, + ), + 'sqlite' => array + ( + 'driver' => 'sqlite', + 'default_expire' => 3600, + 'database' => APPPATH.'cache/kohana-cache.sql3', + 'schema' => 'CREATE TABLE caches(id VARCHAR(127) PRIMARY KEY, tags VARCHAR(255), expiration INTEGER, cache TEXT)', + ), + 'eaccelerator' => array + ( + 'driver' => 'eaccelerator', + ), + 'xcache' => array + ( + 'driver' => 'xcache', + 'default_expire' => 3600, + ), + 'file' => array + ( + 'driver' => 'file', + 'cache_dir' => APPPATH.'cache', + 'default_expire' => 3600, + ) +); \ No newline at end of file diff --git a/includes/kohana/modules/cache/guide/cache.about.md b/includes/kohana/modules/cache/guide/cache.about.md new file mode 100644 index 00000000..b93b11d6 --- /dev/null +++ b/includes/kohana/modules/cache/guide/cache.about.md @@ -0,0 +1,59 @@ +# About Kohana Cache + +[Kohana_Cache] provides a common interface to a variety of caching engines. [Kohana_Cache_Tagging] is +supported where available natively to the cache system. Kohana Cache supports multiple +instances of cache engines through a grouped singleton pattern. + +## Supported cache engines + + * APC ([Cache_Apc]) + * eAccelerator ([Cache_Eaccelerator]) + * File ([Cache_File]) + * Memcached ([Cache_Memcache]) + * Memcached-tags ([Cache_Memcachetag]) + * SQLite ([Cache_Sqlite]) + * Xcache ([Cache_Xcache]) + +## Introduction to caching + +Caching should be implemented with consideration. Generally, caching the result of resources +is faster than reprocessing them. Choosing what, how and when to cache is vital. [PHP APC](http://php.net/manual/en/book.apc.php) is one of the fastest caching systems available, closely followed by [Memcached](http://memcached.org/). [SQLite](http://www.sqlite.org/) and File caching are two of the slowest cache methods, however usually faster than reprocessing +a complex set of instructions. + +Caching engines that use memory are considerably faster than file based alternatives. But +memory is limited whereas disk space is plentiful. If caching large datasets, such as large database result sets, it is best to use file caching. + + [!!] Cache drivers require the relevant PHP extensions to be installed. APC, eAccelerator, Memecached and Xcache all require non-standard PHP extensions. + +## What the Kohana Cache module does (and does not do) + +This module provides a simple abstracted interface to a wide selection of popular PHP cache engines. The caching API provides the basic caching methods implemented across all solutions, memory, network or disk based. Basic key / value storing is supported by all drivers, with additional tagging and garbage collection support where implemented or required. + +_Kohana Cache_ does not provide HTTP style caching for clients (web browsers) and/or proxies (_Varnish_, _Squid_). There are other Kohana modules that provide this functionality. + +## Choosing a cache provider + +Getting and setting values to cache is very simple when using the _Kohana Cache_ interface. The hardest choice is choosing which cache engine to use. When choosing a caching engine, the following criteria must be considered: + + 1. __Does the cache need to be distributed?__ + This is an important consideration as it will severely limit the options available to solutions such as Memcache when a distributed solution is required. + 2. __Does the cache need to be fast?__ + In almost all cases retrieving data from a cache is faster than execution. However generally memory based caching is considerably faster than disk based caching (see table below). + 3. __How much cache is required?__ + Cache is not endless, and memory based caches are subject to a considerably more limited storage resource. + +Driver | Storage | Speed | Tags | Distributed | Automatic Garbage Collection | Notes +---------------- | ------------ | --------- | -------- | ----------- | ---------------------------- | ----------------------- +APC | __Memory__ | Excellent | No | No | Yes | Widely available PHP opcode caching solution, improves php execution performance +eAccelerator | __Memory__ | Excellent | No | No | Yes | Limited support and no longer developed. Included for legacy systems +File | __Disk__ | Poor | No | No | No | Marginally faster than execution +Memcache (tag) | __Memory__ | Good | No (yes) | Yes | Yes | Generally fast distributed solution, but has a speed hit due to variable network latency +Sqlite | __Disk__ | Poor | Yes | No | No | Marginally faster than execution +Xcache | __Memory__ | Excellent | Yes | No | Yes | Very fast memory solution and alternative to APC + +It is possible to have hybrid cache solutions that use a combination of the engines above in different contexts. This is supported with _Kohana Cache_ as well. + +## Minimum requirements + + * Kohana 3.0.4 + * PHP 5.2.4 or greater \ No newline at end of file diff --git a/includes/kohana/modules/cache/guide/cache.config.md b/includes/kohana/modules/cache/guide/cache.config.md new file mode 100644 index 00000000..a6d428fe --- /dev/null +++ b/includes/kohana/modules/cache/guide/cache.config.md @@ -0,0 +1,168 @@ +# Kohana Cache configuration + +Kohana Cache uses configuration groups to create cache instances. A configuration group can +use any supported driver, with successive groups using multiple instances of the same driver type. + +The default cache group is loaded based on the `Cache::$default` setting. It is set to the `file` driver as standard, however this can be changed within the `/application/boostrap.php` file. + + // Change the default cache driver to memcache + Cache::$default = 'memcache'; + + // Load the memcache cache driver using default setting + $memcache = Cache::instance(); + +## Group settings + +Below are the default cache configuration groups for each supported driver. Add to- or override these settings +within the `application/config/cache.php` file. + +Name | Required | Description +-------------- | -------- | --------------------------------------------------------------- +driver | __YES__ | (_string_) The driver type to use +default_expire | __NO__ | (_string_) The driver type to use + + + 'file' => array + ( + 'driver' => 'file', + 'cache_dir' => APPPATH.'cache/.kohana_cache', + 'default_expire' => 3600, + ), + +## Memcache & Memcached-tag settings + +Name | Required | Description +-------------- | -------- | --------------------------------------------------------------- +driver | __YES__ | (_string_) The driver type to use +servers | __YES__ | (_array_) Associative array of server details, must include a __host__ key. (see _Memcache server configuration_ below) +compression | __NO__ | (_boolean_) Use data compression when caching + +### Memcache server configuration + +Name | Required | Description +---------------- | -------- | --------------------------------------------------------------- +host | __YES__ | (_string_) The host of the memcache server, i.e. __localhost__; or __127.0.0.1__; or __memcache.domain.tld__ +port | __NO__ | (_integer_) Point to the port where memcached is listening for connections. Set this parameter to 0 when using UNIX domain sockets. Default __11211__ +persistent | __NO__ | (_boolean_) Controls the use of a persistent connection. Default __TRUE__ +weight | __NO__ | (_integer_) Number of buckets to create for this server which in turn control its probability of it being selected. The probability is relative to the total weight of all servers. Default __1__ +timeout | __NO__ | (_integer_) Value in seconds which will be used for connecting to the daemon. Think twice before changing the default value of 1 second - you can lose all the advantages of caching if your connection is too slow. Default __1__ +retry_interval | __NO__ | (_integer_) Controls how often a failed server will be retried, the default value is 15 seconds. Setting this parameter to -1 disables automatic retry. Default __15__ +status | __NO__ | (_boolean_) Controls if the server should be flagged as online. Default __TRUE__ +failure_callback | __NO__ | (_[callback](http://www.php.net/manual/en/language.pseudo-types.php#language.types.callback)_) Allows the user to specify a callback function to run upon encountering an error. The callback is run before failover is attempted. The function takes two parameters, the hostname and port of the failed server. Default __NULL__ + + 'memcache' => array + ( + 'driver' => 'memcache', + 'default_expire' => 3600, + 'compression' => FALSE, // Use Zlib compression + (can cause issues with integers) + 'servers' => array + ( + array + ( + 'host' => 'localhost', // Memcache Server + 'port' => 11211, // Memcache port number + 'persistent' => FALSE, // Persistent connection + ), + ), + ), + 'memcachetag' => array + ( + 'driver' => 'memcachetag', + 'default_expire' => 3600, + 'compression' => FALSE, // Use Zlib compression + (can cause issues with integers) + 'servers' => array + ( + array + ( + 'host' => 'localhost', // Memcache Server + 'port' => 11211, // Memcache port number + 'persistent' => FALSE, // Persistent connection + ), + ), + ), + +## APC settings + + 'apc' => array + ( + 'driver' => 'apc', + 'default_expire' => 3600, + ), + +## SQLite settings + + 'sqlite' => array + ( + 'driver' => 'sqlite', + 'default_expire' => 3600, + 'database' => APPPATH.'cache/kohana-cache.sql3', + 'schema' => 'CREATE TABLE caches(id VARCHAR(127) PRIMARY KEY, + tags VARCHAR(255), expiration INTEGER, cache TEXT)', + ), + +## Eaccelerator settings + + 'eaccelerator' array + ( + 'driver' => 'eaccelerator', + ), + +## Xcache settings + + 'xcache' => array + ( + 'driver' => 'xcache', + 'default_expire' => 3600, + ), + +## File settings + + 'file' => array + ( + 'driver' => 'file', + 'cache_dir' => 'cache/.kohana_cache', + 'default_expire' => 3600, + ) + +## Override existing configuration group + +The following example demonstrates how to override an existing configuration setting, using the config file in `/application/config/cache.php`. + + array + ( + 'driver' => 'memcache', // Use Memcached as the default driver + 'default_expire' => 8000, // Overide default expiry + 'servers' => array + ( + // Add a new server + array + ( + 'host' => 'cache.domain.tld', + 'port' => 11211, + 'persistent' => FALSE + ) + ), + 'compression' => FALSE + ) + ); + +## Add new configuration group + +The following example demonstrates how to add a new configuration setting, using the config file in `/application/config/cache.php`. + + array + ( + 'driver' => 'apc', // Use Memcached as the default driver + 'default_expire' => 1000, // Overide default expiry + ) + ); \ No newline at end of file diff --git a/includes/kohana/modules/cache/guide/cache.usage.md b/includes/kohana/modules/cache/guide/cache.usage.md new file mode 100644 index 00000000..8a8239bc --- /dev/null +++ b/includes/kohana/modules/cache/guide/cache.usage.md @@ -0,0 +1,219 @@ +# Kohana Cache usage + +[Kohana_Cache] provides a simple interface allowing getting, setting and deleting of cached values. Two interfaces included in _Kohana Cache_ additionally provide _tagging_ and _garbage collection_ where they are supported by the respective drivers. + +## Getting a new cache instance + +Creating a new _Kohana Cache_ instance is simple, however it must be done using the [Cache::instance] method, rather than the traditional `new` constructor. + + // Create a new instance of cache using the default group + $cache = Cache::instance(); + +The default group will use whatever is set to [Cache::$default] and must have a corresponding [configuration](cache.config) definition for that group. + +To create a cache instance using a group other than the _default_, simply provide the group name as an argument. + + // Create a new instance of the memcache group + $memcache = Cache::instance('memcache'); + +If there is a cache instance already instantiated then you can get it directly from the class member. + + [!!] Beware that this can cause issues if you do not test for the instance before trying to access it. + + // Check for the existance of the cache driver + if (isset(Cache::$instances['memcache'])) + { + // Get the existing cache instance directly (faster) + $memcache = Cache::$instances['memcache']; + } + else + { + // Get the cache driver instance (slower) + $memcache = Cache::instance('memcache'); + } + +## Setting and getting variables to and from cache + +The cache library supports scalar and object values, utilising object serialization where required (or not supported by the caching engine). This means that the majority or objects can be cached without any modification. + + [!!] Serialisation does not work with resource handles, such as filesystem, curl or socket resources. + +### Setting a value to cache + +Setting a value to cache using the [Cache::set] method can be done in one of two ways; either using the Cache instance interface, which is good for atomic operations; or getting an instance and using that for multiple operations. + +The first example demonstrates how to quickly load and set a value to the default cache instance. + + // Create a cachable object + $object = new stdClass; + + // Set a property + $object->foo = 'bar'; + + // Cache the object using default group (quick interface) with default time (3600 seconds) + Cache::instance()->set('foo', $object); + +If multiple cache operations are required, it is best to assign an instance of Cache to a variable and use that as below. + + // Set the object using a defined group for a defined time period (30 seconds) + $memcache = Cache::instance('memcache'); + $memcache->set('foo', $object, 30); + +#### Setting a value with tags + +Certain cache drivers support setting values with tags. To set a value to cache with tags using the following interface. + + // Get a cache instance that supports tags + $memcache = Cache::instance('memcachetag'); + + // Test for tagging interface + if ($memcache instanceof Kohana_Cache_Tagging) + { + // Set a value with some tags for 30 seconds + $memcache->set('foo', $object, 30, array('snafu', 'stfu', 'fubar')); + } + // Otherwise set without tags + else + { + // Set a value for 30 seconds + $memcache->set('foo', $object, 30); + } + +It is possible to implement custom tagging solutions onto existing or new cache drivers by implementing the [Kohana_Cache_Tagging] interface. Kohana_Cache only applies the interface to drivers that support tagging natively as standard. + +### Getting a value from cache + +Getting variables back from cache is achieved using the [Cache::get] method using a single key to identify the cache entry. + + // Retrieve a value from cache (quickly) + $object = Cache::instance()->get('foo'); + +In cases where the requested key is not available or the entry has expired, a default value will be returned (__NULL__ by default). It is possible to define the default value as the key is requested. + + // If the cache key is available (with default value set to FALSE) + if ($object = Cache::instance()->get('foo', FALSE)) + { + // Do something + } + else + { + // Do something else + } + +#### Getting values from cache using tags + +It is possible to retrieve values from cache grouped by tag, using the [Cache::find] method with drivers that support tagging. + + [!!] The __Memcachetag__ driver does not support the `Cache::find($tag)` interface and will throw an exception. + + // Get an instance of cache + $cache = Cache::instance('memcachetag'); + + // Wrap in a try/catch statement to gracefully handle memcachetag + try + { + // Find values based on tag + return $cache->find('snafu'); + } + catch (Kohana_Cache_Exception $e) + { + // Handle gracefully + return FALSE; + } + +### Deleting values from cache + +Deleting variables is very similar to the getting and setting methods already described. Deleting operations are split into three categories: + + - __Delete value by key__. Deletes a cached value by the associated key. + - __Delete all values__. Deletes all caches values stored in the cache instance. + - __Delete values by tag__. Deletes all values that have the supplied tag. This is only supported by Memcached-Tag and Sqlite. + +#### Delete value by key + +To delete a specific value by its associated key: + + // If the cache entry for 'foo' is deleted + if (Cache::instance()->delete('foo')) + { + // Cache entry successfully deleted, do something + } + +By default a `TRUE` value will be returned. However a `FALSE` value will be returned in instances where the key did not exist in the cache. + +#### Delete all values + +To delete all values in a specific instance: + + // If all cache items where deleted successfully + if (Cache::instance()->delete_all()) + { + // Do something + } + +It is also possible to delete all cache items in every instance: + + // For each cache instance + foreach (Cache::$instances as $group => $instance) + { + if ($instance->delete_all()) + { + var_dump('instance : '.$group.' has been flushed!'); + } + } + +#### Delete values by tag + +Some of the caching drivers support deleting by tag. This will remove all the cached values that are associated with a specific tag. Below is an example of how to robustly handle deletion by tag. + + // Get cache instance + $cache = Cache::instance(); + + // Check for tagging interface + if ($cache instanceof Kohana_Cache_Tagging) + { + // Delete all entries by the tag 'snafu' + $cache->delete_tag('snafu'); + } + +#### Garbage Collection + +Garbage Collection (GC) is the cleaning of expired cache entries. For the most part, caching engines will take care of garbage collection internally. However a few of the file based systems do not handle this task and in these circumstances it would be prudent to garbage collect at a predetermined frequency. If no garbage collection is executed, the resource storing the cache entries will eventually fill and become unusable. + +When not automated, garbage collection is the responsibility of the developer. It is prudent to have a GC probability value that dictates how likely the garbage collection routing will be run. An example of such a system is demonstrated below. + + // Get a cache instance + $cache_file = Cache::instance('file'); + + // Set a GC probability of 10% + $gc = 10; + + // If the GC probability is a hit + if (rand(0,99) <= $gc and $cache_file instanceof Kohana_Cache_GarbageCollect) + { + // Garbage Collect + $cache_file->garbage_collect(); + } + +# Interfaces + +Kohana Cache comes with two interfaces that are implemented where the drivers support them: + + - __[Kohana_Cache_Tagging] for tagging support on cache entries__ + - [Cache_MemcacheTag] + - [Cache_Sqlite] + - __[Kohana_Cache_GarbageCollect] for garbage collection with drivers without native support__ + - [Cache_File] + - [Cache_Sqlite] + +When using interface specific caching features, ensure that code checks for the required interface before using the methods supplied. The following example checks whether the garbage collection interface is available before calling the `garbage_collect` method. + + // Create a cache instance + $cache = Cache::instance(); + + // Test for Garbage Collection + if ($cache instanceof Kohana_Cache_GarbageCollect) + { + // Collect garbage + $cache->garbage_collect(); + } \ No newline at end of file diff --git a/includes/kohana/modules/cache/guide/menu.cache.md b/includes/kohana/modules/cache/guide/menu.cache.md new file mode 100644 index 00000000..3f516e5c --- /dev/null +++ b/includes/kohana/modules/cache/guide/menu.cache.md @@ -0,0 +1,4 @@ +1. **Cache** + - [About](cache.about) + - [Configuration](cache.config) + - [Usage](cache.usage) \ No newline at end of file diff --git a/includes/kohana/modules/cache/tests/cache/KohanaCacheTest.php b/includes/kohana/modules/cache/tests/cache/KohanaCacheTest.php new file mode 100644 index 00000000..229e7d32 --- /dev/null +++ b/includes/kohana/modules/cache/tests/cache/KohanaCacheTest.php @@ -0,0 +1,91 @@ +delete_all(); + + self::$test_instance->set('testGet1', 'foo', 3600); + } + + public function tearDown() + { + self::$test_instance->delete_all(); + self::$test_instance = NULL; + } + + /** + * Tests the cache static instance method + */ + public function testInstance() + { + $file_instance = Cache::instance('file'); + $file_instance2 = Cache::instance('file'); + + // Try and load a Cache instance + $this->assertType('Kohana_Cache', Cache::instance()); + $this->assertType('Kohana_Cache_File', $file_instance); + + // Test instances are only initialised once + $this->assertTrue(spl_object_hash($file_instance) == spl_object_hash($file_instance2)); + + // Test the publically accessible Cache instance store + $this->assertTrue(spl_object_hash(Cache::$instances['file']) == spl_object_hash($file_instance)); + + // Get the constructor method + $constructorMethod = new ReflectionMethod($file_instance, '__construct'); + + // Test the constructor for hidden visibility + $this->assertTrue($constructorMethod->isProtected(), '__construct is does not have protected visibility'); + } + + public function testGet() + { + // Try and get a non property + $this->assertNull(self::$test_instance->get('testGet0')); + + // Try and get a non property with default return value + $this->assertEquals('bar', self::$test_instance->get('testGet0', 'bar')); + + // Try and get a real cached property + $this->assertEquals('foo', self::$test_instance->get('testGet1')); + } + + public function testSet() + { + $value = 'foobar'; + $value2 = 'snafu'; + + // Set a new property + $this->assertTrue(self::$test_instance->set('testSet1', $value)); + + // Test the property exists + $this->assertEquals(self::$test_instance->get('testSet1'), $value); + + // Test short set + $this->assertTrue(self::$test_instance->set('testSet2', $value2, 3)); + + // Test the property exists + $this->assertEquals(self::$test_instance->get('testSet2'), $value2); + + // Allow test2 to expire + sleep(4); + + // Test the property has expired + $this->assertNull(self::$test_instance->get('testSet2')); + } + + public function testDelete() + { + + } + + public function testDeleteAll() + { + + } +} \ No newline at end of file diff --git a/includes/kohana/modules/cache/tests/phpunit.xml b/includes/kohana/modules/cache/tests/phpunit.xml new file mode 100644 index 00000000..5e3b9c79 --- /dev/null +++ b/includes/kohana/modules/cache/tests/phpunit.xml @@ -0,0 +1,16 @@ + + + + + cache/ + + + diff --git a/includes/kohana/modules/codebench/classes/bench/arrcallback.php b/includes/kohana/modules/codebench/classes/bench/arrcallback.php new file mode 100644 index 00000000..698a6b8d --- /dev/null +++ b/includes/kohana/modules/codebench/classes/bench/arrcallback.php @@ -0,0 +1,57 @@ + + */ +class Bench_ArrCallback extends Codebench { + + public $description = + 'Parsing command[param,param] strings in Arr::callback(): + http://github.com/shadowhand/kohana/commit/c3aaae849164bf92a486e29e736a265b350cb4da#L0R127'; + + public $loops = 10000; + + public $subjects = array + ( + // Valid callback strings + 'foo', + 'foo::bar', + 'foo[apple,orange]', + 'foo::bar[apple,orange]', + '[apple,orange]', // no command, only params + 'foo[[apple],[orange]]', // params with brackets inside + + // Invalid callback strings + 'foo[apple,orange', // no closing bracket + ); + + public function bench_shadowhand($subject) + { + // The original regex we're trying to optimize + if (preg_match('/([^\[]*+)\[(.*)\]/', $subject, $match)) + return $match; + } + + public function bench_geert_regex_1($subject) + { + // Added ^ and $ around the whole pattern + if (preg_match('/^([^\[]*+)\[(.*)\]$/', $subject, $matches)) + return $matches; + } + + public function bench_geert_regex_2($subject) + { + // A rather experimental approach using \K which requires PCRE 7.2 ~ PHP 5.2.4 + // Note: $matches[0] = params, $matches[1] = command + if (preg_match('/^([^\[]*+)\[\K.*(?=\]$)/', $subject, $matches)) + return $matches; + } + + public function bench_geert_str($subject) + { + // A native string function approach which beats all the regexes + if (strpos($subject, '[') !== FALSE AND substr($subject, -1) === ']') + return explode('[', substr($subject, 0, -1), 2); + } +} \ No newline at end of file diff --git a/includes/kohana/modules/codebench/classes/bench/autolinkemails.php b/includes/kohana/modules/codebench/classes/bench/autolinkemails.php new file mode 100644 index 00000000..46e7a158 --- /dev/null +++ b/includes/kohana/modules/codebench/classes/bench/autolinkemails.php @@ -0,0 +1,70 @@ + + */ +class Bench_AutoLinkEmails extends Codebench { + + public $description = + 'Fixing #2772, and comparing some possibilities.'; + + public $loops = 1000; + + public $subjects = array + ( + '
    +
  • voorzitter@xxxx.com
  • +
  • vicevoorzitter@xxxx.com
  • +
', + ); + + // The original function, with str_replace replaced by preg_replace. Looks clean. + public function bench_match_all_loop($subject) + { + if (preg_match_all('~\b(?|58;)(?!\.)[-+_a-z0-9.]++(?|58;)(?!\.)[-+_a-z0-9.]++(?|58;)(?!\.)[-+_a-z0-9.]++(?|58;)(?!\.)[-+_a-z0-9.]++(? + */ +class Bench_DateSpan extends Codebench { + + public $description = + 'Optimization for Date::span().'; + + public $loops = 1000; + + public $subjects = array(); + + public function __construct() + { + parent::__construct(); + + $this->subjects = array( + time(), + time() - Date::MONTH, + time() - Date::YEAR, + time() - Date::YEAR * 10, + ); + } + + // Original method + public static function bench_span_original($remote, $local = NULL, $output = 'years,months,weeks,days,hours,minutes,seconds') + { + // Array with the output formats + $output = preg_split('/[^a-z]+/', strtolower((string) $output)); + + // Invalid output + if (empty($output)) + return FALSE; + + // Make the output values into keys + extract(array_flip($output), EXTR_SKIP); + + if ($local === NULL) + { + // Calculate the span from the current time + $local = time(); + } + + // Calculate timespan (seconds) + $timespan = abs($remote - $local); + + if (isset($years)) + { + $timespan -= Date::YEAR * ($years = (int) floor($timespan / Date::YEAR)); + } + + if (isset($months)) + { + $timespan -= Date::MONTH * ($months = (int) floor($timespan / Date::MONTH)); + } + + if (isset($weeks)) + { + $timespan -= Date::WEEK * ($weeks = (int) floor($timespan / Date::WEEK)); + } + + if (isset($days)) + { + $timespan -= Date::DAY * ($days = (int) floor($timespan / Date::DAY)); + } + + if (isset($hours)) + { + $timespan -= Date::HOUR * ($hours = (int) floor($timespan / Date::HOUR)); + } + + if (isset($minutes)) + { + $timespan -= Date::MINUTE * ($minutes = (int) floor($timespan / Date::MINUTE)); + } + + // Seconds ago, 1 + if (isset($seconds)) + { + $seconds = $timespan; + } + + // Remove the variables that cannot be accessed + unset($timespan, $remote, $local); + + // Deny access to these variables + $deny = array_flip(array('deny', 'key', 'difference', 'output')); + + // Return the difference + $difference = array(); + foreach ($output as $key) + { + if (isset($$key) AND ! isset($deny[$key])) + { + // Add requested key to the output + $difference[$key] = $$key; + } + } + + // Invalid output formats string + if (empty($difference)) + return FALSE; + + // If only one output format was asked, don't put it in an array + if (count($difference) === 1) + return current($difference); + + // Return array + return $difference; + } + + // Using an array for the output + public static function bench_span_use_array($remote, $local = NULL, $output = 'years,months,weeks,days,hours,minutes,seconds') + { + // Array with the output formats + $output = preg_split('/[^a-z]+/', strtolower((string) $output)); + + // Invalid output + if (empty($output)) + return FALSE; + + // Convert the list of outputs to an associative array + $output = array_combine($output, array_fill(0, count($output), 0)); + + // Make the output values into keys + extract(array_flip($output), EXTR_SKIP); + + if ($local === NULL) + { + // Calculate the span from the current time + $local = time(); + } + + // Calculate timespan (seconds) + $timespan = abs($remote - $local); + + if (isset($output['years'])) + { + $timespan -= Date::YEAR * ($output['years'] = (int) floor($timespan / Date::YEAR)); + } + + if (isset($output['months'])) + { + $timespan -= Date::MONTH * ($output['months'] = (int) floor($timespan / Date::MONTH)); + } + + if (isset($output['weeks'])) + { + $timespan -= Date::WEEK * ($output['weeks'] = (int) floor($timespan / Date::WEEK)); + } + + if (isset($output['days'])) + { + $timespan -= Date::DAY * ($output['days'] = (int) floor($timespan / Date::DAY)); + } + + if (isset($output['hours'])) + { + $timespan -= Date::HOUR * ($output['hours'] = (int) floor($timespan / Date::HOUR)); + } + + if (isset($output['minutes'])) + { + $timespan -= Date::MINUTE * ($output['minutes'] = (int) floor($timespan / Date::MINUTE)); + } + + // Seconds ago, 1 + if (isset($output['seconds'])) + { + $output['seconds'] = $timespan; + } + + if (count($output) === 1) + { + // Only a single output was requested, return it + return array_pop($output); + } + + // Return array + return $output; + } + +} \ No newline at end of file diff --git a/includes/kohana/modules/codebench/classes/bench/explodelimit.php b/includes/kohana/modules/codebench/classes/bench/explodelimit.php new file mode 100644 index 00000000..4bf2acc2 --- /dev/null +++ b/includes/kohana/modules/codebench/classes/bench/explodelimit.php @@ -0,0 +1,34 @@ + + */ +class Bench_ExplodeLimit extends Codebench { + + public $description = + 'Having a look at the effect of adding a limit to the explode function.
+ http://stackoverflow.com/questions/1308149/how-to-get-a-part-of-url-between-4th-and-5th-slashes'; + + public $loops = 10000; + + public $subjects = array + ( + 'http://example.com/articles/123a/view', + 'http://example.com/articles/123a/view/x/x/x/x/x', + 'http://example.com/articles/123a/view/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x/x', + ); + + public function bench_explode_without_limit($subject) + { + $parts = explode('/', $subject); + return $parts[4]; + } + + public function bench_explode_with_limit($subject) + { + $parts = explode('/', $subject, 6); + return $parts[4]; + } + +} \ No newline at end of file diff --git a/includes/kohana/modules/codebench/classes/bench/gruberurl.php b/includes/kohana/modules/codebench/classes/bench/gruberurl.php new file mode 100644 index 00000000..af239750 --- /dev/null +++ b/includes/kohana/modules/codebench/classes/bench/gruberurl.php @@ -0,0 +1,61 @@ + + */ +class Bench_GruberURL extends Codebench { + + public $description = + 'Optimization for http://daringfireball.net/2009/11/liberal_regex_for_matching_urls'; + + public $loops = 10000; + + public $subjects = array + ( + 'http://foo.com/blah_blah', + 'http://foo.com/blah_blah/', + '(Something like http://foo.com/blah_blah)', + 'http://foo.com/blah_blah_(wikipedia)', + '(Something like http://foo.com/blah_blah_(wikipedia))', + 'http://foo.com/blah_blah.', + 'http://foo.com/blah_blah/.', + '', + '', + 'http://foo.com/blah_blah,', + 'http://www.example.com/wpstyle/?p=364.', + 'http://✪df.ws/e7l', + 'rdar://1234', + 'rdar:/1234', + 'x-yojimbo-item://6303E4C1-xxxx-45A6-AB9D-3A908F59AE0E', + 'message://%3c330e7f8409726r6a4ba78dkf1fd71420c1bf6ff@mail.gmail.com%3e', + 'http://➡.ws/䨹', + 'www.➡.ws/䨹', + 'http://example.com', + 'Just a www.example.com link.', + // To test the use of possessive quatifiers: + 'httpppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp', + ); + + public function bench_daringfireball($subject) + { + // Original regex by John Gruber + preg_match('~\b(([\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/)))~', $subject, $matches); + return (empty($matches)) ? FALSE : $matches[0]; + } + + public function bench_daringfireball_v2($subject) + { + // Removed outer capturing parentheses, made another pair non-capturing + preg_match('~\b(?:[\w-]+://?|www[.])[^\s()<>]+(?:\([\w\d]+\)|(?:[^[:punct:]\s]|/))~', $subject, $matches); + return (empty($matches)) ? FALSE : $matches[0]; + } + + public function bench_daringfireball_v3($subject) + { + // Made quantifiers possessive where possible + preg_match('~\b(?:[\w-]++://?+|www[.])[^\s()<>]+(?:\([\w\d]++\)|(?:[^[:punct:]\s]|/))~', $subject, $matches); + return (empty($matches)) ? FALSE : $matches[0]; + } + +} \ No newline at end of file diff --git a/includes/kohana/modules/codebench/classes/bench/ltrimdigits.php b/includes/kohana/modules/codebench/classes/bench/ltrimdigits.php new file mode 100644 index 00000000..71ead49c --- /dev/null +++ b/includes/kohana/modules/codebench/classes/bench/ltrimdigits.php @@ -0,0 +1,28 @@ + + */ +class Bench_LtrimDigits extends Codebench { + + public $description = 'Chopping off leading digits: regex vs ltrim.'; + + public $loops = 100000; + + public $subjects = array + ( + '123digits', + 'no-digits', + ); + + public function bench_regex($subject) + { + return preg_replace('/^\d+/', '', $subject); + } + + public function bench_ltrim($subject) + { + return ltrim($subject, '0..9'); + } +} \ No newline at end of file diff --git a/includes/kohana/modules/codebench/classes/bench/mddobaseurl.php b/includes/kohana/modules/codebench/classes/bench/mddobaseurl.php new file mode 100644 index 00000000..1ad2a1b7 --- /dev/null +++ b/includes/kohana/modules/codebench/classes/bench/mddobaseurl.php @@ -0,0 +1,66 @@ + + */ +class Bench_MDDoBaseURL extends Codebench { + + public $description = + 'Optimization for the doBaseURL() method of Kohana_Kodoc_Markdown + for the Kohana Userguide.'; + + public $loops = 10000; + + public $subjects = array + ( + // Valid matches + '[filesystem](about.filesystem)', + '[filesystem](about.filesystem "Optional title")', + '[same page link](#id)', + '[object oriented](http://wikipedia.org/wiki/Object-Oriented_Programming)', + + // Invalid matches + '![this is image syntax](about.filesystem)', + '[filesystem](about.filesystem', + ); + + public function bench_original($subject) + { + // The original regex contained a bug, which is fixed here for benchmarking purposes. + // At the very start of the regex, (?!!) has been replace by (? + */ +class Bench_MDDoImageURL extends Codebench { + + public $description = + 'Optimization for the doImageURL() method of Kohana_Kodoc_Markdown + for the Kohana Userguide.'; + + public $loops = 10000; + + public $subjects = array + ( + // Valid matches + '![Alt text](http://img.skitch.com/20091019-rud5mmqbf776jwua6hx9nm1n.png)', + '![Alt text](https://img.skitch.com/20091019-rud5mmqbf776jwua6hx9nm1n.png)', + '![Alt text](otherprotocol://image.png "Optional title")', + '![Alt text](img/install.png "Optional title")', + '![Alt text containing [square] brackets](img/install.png)', + '![Empty src]()', + + // Invalid matches + '![Alt text](img/install.png "No closing parenthesis"', + ); + + public function bench_original($subject) + { + return preg_replace_callback('~!\[(.+?)\]\((\S*(?:\s*".+?")?)\)~', array($this, '_add_image_url_original'), $subject); + } + protected function _add_image_url_original($matches) + { + if ($matches[2] AND strpos($matches[2], '://') === FALSE) + { + // Add the base url to the link URL + $matches[2] = 'http://BASE/'.$matches[2]; + } + + // Recreate the link + return "![{$matches[1]}]({$matches[2]})"; + } + + public function bench_optimized_callback($subject) + { + // Moved the check for "://" to the regex, simplifying the callback function + return preg_replace_callback('~!\[(.+?)\]\((?!\w++://)(\S*(?:\s*+".+?")?)\)~', array($this, '_add_image_url_optimized'), $subject); + } + protected function _add_image_url_optimized($matches) + { + // Add the base url to the link URL + $matches[2] = 'http://BASE/'.$matches[2]; + + // Recreate the link + return "![{$matches[1]}]({$matches[2]})"; + } + + public function bench_callback_gone($subject) + { + // All the optimized callback was doing now, is prepend some text to the URL. + // We don't need a callback for that, and that should be clearly faster. + return preg_replace('~(!\[.+?\]\()(?!\w++://)(\S*(?:\s*+".+?")?\))~', '$1http://BASE/$2', $subject); + } + +} \ No newline at end of file diff --git a/includes/kohana/modules/codebench/classes/bench/mddoincludeviews.php b/includes/kohana/modules/codebench/classes/bench/mddoincludeviews.php new file mode 100644 index 00000000..9cac3d60 --- /dev/null +++ b/includes/kohana/modules/codebench/classes/bench/mddoincludeviews.php @@ -0,0 +1,50 @@ + + */ +class Bench_MDDoIncludeViews extends Codebench { + + public $description = + 'Optimization for the doIncludeViews() method of Kohana_Kodoc_Markdown + for the Kohana Userguide.'; + + public $loops = 10000; + + public $subjects = array + ( + // Valid matches + '{{one}} two {{three}}', + '{{userguide/examples/hello_world_error}}', + + // Invalid matches + '{}', + '{{}}', + '{{userguide/examples/hello_world_error}', + '{{userguide/examples/hello_world_error }}', + '{{userguide/examples/{{hello_world_error }}', + ); + + public function bench_original($subject) + { + preg_match_all('/{{(\S+?)}}/m', $subject, $matches, PREG_SET_ORDER); + return $matches; + } + + public function bench_possessive($subject) + { + // Using a possessive character class + // Removed useless /m modifier + preg_match_all('/{{([^\s{}]++)}}/', $subject, $matches, PREG_SET_ORDER); + return $matches; + } + + public function bench_lookaround($subject) + { + // Using lookaround to move $mathes[1] into $matches[0] + preg_match_all('/(?<={{)[^\s{}]++(?=}})/', $subject, $matches, PREG_SET_ORDER); + return $matches; + } + +} \ No newline at end of file diff --git a/includes/kohana/modules/codebench/classes/bench/stripnullbytes.php b/includes/kohana/modules/codebench/classes/bench/stripnullbytes.php new file mode 100644 index 00000000..4d28853e --- /dev/null +++ b/includes/kohana/modules/codebench/classes/bench/stripnullbytes.php @@ -0,0 +1,37 @@ + + */ +class Bench_StripNullBytes extends Codebench { + + public $description = + 'String replacement comparisons related to #2676.'; + + public $loops = 1000; + + public $subjects = array + ( + "\0", + "\0\0\0\0\0\0\0\0\0\0", + "bla\0bla\0bla\0bla\0bla\0bla\0bla\0bla\0bla\0bla", + "blablablablablablablablablablablablablablablabla", + ); + + public function bench_str_replace($subject) + { + return str_replace("\0", '', $subject); + } + + public function bench_strtr($subject) + { + return strtr($subject, array("\0" => '')); + } + + public function bench_preg_replace($subject) + { + return preg_replace('~\0+~', '', $subject); + } + +} \ No newline at end of file diff --git a/includes/kohana/modules/codebench/classes/bench/transliterate.php b/includes/kohana/modules/codebench/classes/bench/transliterate.php new file mode 100644 index 00000000..aff86931 --- /dev/null +++ b/includes/kohana/modules/codebench/classes/bench/transliterate.php @@ -0,0 +1,65 @@ + + */ +class Bench_Transliterate extends Codebench { + + public $description = + 'Inspired by: + http://forum.kohanaframework.org/comments.php?DiscussionID=6113'; + + public $loops = 10; + + public $subjects = array + ( + // ASCII + 'a', 'b', 'c', 'd', '1', '2', '3', + + // Non-ASCII + 'à', 'ô', 'ď', 'ḟ', 'ë', 'š', 'ơ', + 'ß', 'ă', 'ř', 'ț', 'ň', 'ā', 'ķ', + 'ŝ', 'ỳ', 'ņ', 'ĺ', 'ħ', 'ṗ', 'ó', + 'ú', 'ě', 'é', 'ç', 'ẁ', 'ċ', 'õ', + 'ṡ', 'ø', 'ģ', 'ŧ', 'ș', 'ė', 'ĉ', + 'ś', 'î', 'ű', 'ć', 'ę', 'ŵ', 'ṫ', + 'ū', 'č', 'ö', 'è', 'ŷ', 'ą', 'ł', + 'ų', 'ů', 'ş', 'ğ', 'ļ', 'ƒ', 'ž', + 'ẃ', 'ḃ', 'å', 'ì', 'ï', 'ḋ', 'ť', + 'ŗ', 'ä', 'í', 'ŕ', 'ê', 'ü', 'ò', + 'ē', 'ñ', 'ń', 'ĥ', 'ĝ', 'đ', 'ĵ', + 'ÿ', 'ũ', 'ŭ', 'ư', 'ţ', 'ý', 'ő', + 'â', 'ľ', 'ẅ', 'ż', 'ī', 'ã', 'ġ', + 'ṁ', 'ō', 'ĩ', 'ù', 'į', 'ź', 'á', + 'û', 'þ', 'ð', 'æ', 'µ', 'ĕ', 'ı', + 'À', 'Ô', 'Ď', 'Ḟ', 'Ë', 'Š', 'Ơ', + 'Ă', 'Ř', 'Ț', 'Ň', 'Ā', 'Ķ', 'Ĕ', + 'Ŝ', 'Ỳ', 'Ņ', 'Ĺ', 'Ħ', 'Ṗ', 'Ó', + 'Ú', 'Ě', 'É', 'Ç', 'Ẁ', 'Ċ', 'Õ', + 'Ṡ', 'Ø', 'Ģ', 'Ŧ', 'Ș', 'Ė', 'Ĉ', + 'Ś', 'Î', 'Ű', 'Ć', 'Ę', 'Ŵ', 'Ṫ', + 'Ū', 'Č', 'Ö', 'È', 'Ŷ', 'Ą', 'Ł', + 'Ų', 'Ů', 'Ş', 'Ğ', 'Ļ', 'Ƒ', 'Ž', + 'Ẃ', 'Ḃ', 'Å', 'Ì', 'Ï', 'Ḋ', 'Ť', + 'Ŗ', 'Ä', 'Í', 'Ŕ', 'Ê', 'Ü', 'Ò', + 'Ē', 'Ñ', 'Ń', 'Ĥ', 'Ĝ', 'Đ', 'Ĵ', + 'Ÿ', 'Ũ', 'Ŭ', 'Ư', 'Ţ', 'Ý', 'Ő', + 'Â', 'Ľ', 'Ẅ', 'Ż', 'Ī', 'Ã', 'Ġ', + 'Ṁ', 'Ō', 'Ĩ', 'Ù', 'Į', 'Ź', 'Á', + 'Û', 'Þ', 'Ð', 'Æ', 'İ', + ); + + public function bench_utf8($subject) + { + return UTF8::transliterate_to_ascii($subject); + } + + public function bench_iconv($subject) + { + // Note: need to suppress errors on iconv because some chars trigger the following notice: + // "Detected an illegal character in input string" + return preg_replace('~[^-a-z0-9]+~i', '', @iconv('UTF-8', 'ASCII//TRANSLIT', $subject)); + } + +} \ No newline at end of file diff --git a/includes/kohana/modules/codebench/classes/bench/urlsite.php b/includes/kohana/modules/codebench/classes/bench/urlsite.php new file mode 100644 index 00000000..0db347d8 --- /dev/null +++ b/includes/kohana/modules/codebench/classes/bench/urlsite.php @@ -0,0 +1,123 @@ + + */ +class Bench_URLSite extends Codebench { + + public $description = 'http://dev.kohanaframework.org/issues/3110'; + + public $loops = 1000; + + public $subjects = array + ( + '', + 'news', + 'news/', + '/news/', + 'news/page/5', + 'news/page:5', + 'http://example.com/', + 'http://example.com/hello', + 'http://example.com:80/', + 'http://user:pass@example.com/', + ); + + public function __construct() + { + foreach ($this->subjects as $subject) + { + // Automatically create URIs with query string and/or fragment part appended + $this->subjects[] = $subject.'?query=string'; + $this->subjects[] = $subject.'#fragment'; + $this->subjects[] = $subject.'?query=string#fragment'; + } + + parent::__construct(); + } + + public function bench_original($uri) + { + // Get the path from the URI + $path = trim(parse_url($uri, PHP_URL_PATH), '/'); + + if ($query = parse_url($uri, PHP_URL_QUERY)) + { + $query = '?'.$query; + } + + if ($fragment = parse_url($uri, PHP_URL_FRAGMENT)) + { + $fragment = '#'.$fragment; + } + + return $path.$query.$fragment; + } + + public function bench_explode($uri) + { + // Chop off possible scheme, host, port, user and pass parts + $path = preg_replace('~^[-a-z0-9+.]++://[^/]++/?~', '', trim($uri, '/')); + + $fragment = ''; + $explode = explode('#', $path, 2); + if (isset($explode[1])) + { + $path = $explode[0]; + $fragment = '#'.$explode[1]; + } + + $query = ''; + $explode = explode('?', $path, 2); + if (isset($explode[1])) + { + $path = $explode[0]; + $query = '?'.$explode[1]; + } + + return $path.$query.$fragment; + } + + public function bench_regex($uri) + { + preg_match('~^(?:[-a-z0-9+.]++://[^/]++/?)?([^?#]++)?(\?[^#]*+)?(#.*)?~', trim($uri, '/'), $matches); + $path = Arr::get($matches, 1, ''); + $query = Arr::get($matches, 2, ''); + $fragment = Arr::get($matches, 3, ''); + + return $path.$query.$fragment; + } + + public function bench_regex_without_arrget($uri) + { + preg_match('~^(?:[-a-z0-9+.]++://[^/]++/?)?([^?#]++)?(\?[^#]*+)?(#.*)?~', trim($uri, '/'), $matches); + $path = isset($matches[1]) ? $matches[1] : ''; + $query = isset($matches[2]) ? $matches[2] : ''; + $fragment = isset($matches[3]) ? $matches[3] : ''; + + return $path.$query.$fragment; + } + + // And then I thought, why do all the work of extracting the query and fragment parts and then reappending them? + // Just leaving them alone should be fine, right? As a bonus we get a very nice speed boost. + public function bench_less_is_more($uri) + { + // Chop off possible scheme, host, port, user and pass parts + $path = preg_replace('~^[-a-z0-9+.]++://[^/]++/?~', '', trim($uri, '/')); + + return $path; + } + + public function bench_less_is_more_with_strpos_optimization($uri) + { + if (strpos($uri, '://') !== FALSE) + { + // Chop off possible scheme, host, port, user and pass parts + $uri = preg_replace('~^[-a-z0-9+.]++://[^/]++/?~', '', trim($uri, '/')); + } + + return $uri; + } + +} \ No newline at end of file diff --git a/includes/kohana/modules/codebench/classes/bench/userfuncarray.php b/includes/kohana/modules/codebench/classes/bench/userfuncarray.php new file mode 100644 index 00000000..f53d0c66 --- /dev/null +++ b/includes/kohana/modules/codebench/classes/bench/userfuncarray.php @@ -0,0 +1,58 @@ + + */ +class Bench_UserFuncArray extends Codebench { + + public $description = + 'Testing the speed difference of using call_user_func_array + compared to counting args and doing manual calls.'; + + public $loops = 100000; + + public $subjects = array + ( + // Argument sets + array(), + array('one'), + array('one', 'two'), + array('one', 'two', 'three'), + ); + + public function bench_count_args($args) + { + $name = 'callme'; + switch (count($args)) + { + case 1: + $this->$name($args[0]); + break; + case 2: + $this->$name($args[0], $args[1]); + break; + case 3: + $this->$name($args[0], $args[1], $args[2]); + break; + case 4: + $this->$name($args[0], $args[1], $args[2], $args[3]); + break; + default: + call_user_func_array(array($this, $name), $args); + break; + } + } + + public function bench_direct_call($args) + { + $name = 'callme'; + call_user_func_array(array($this, $name), $args); + } + + protected function callme() + { + return count(func_get_args()); + } + +} \ No newline at end of file diff --git a/includes/kohana/modules/codebench/classes/bench/validcolor.php b/includes/kohana/modules/codebench/classes/bench/validcolor.php new file mode 100644 index 00000000..8d046089 --- /dev/null +++ b/includes/kohana/modules/codebench/classes/bench/validcolor.php @@ -0,0 +1,116 @@ + + */ +class Bench_ValidColor extends Codebench { + + public $description = + 'Optimization for Validate::color(). + See: http://forum.kohanaphp.com/comments.php?DiscussionID=2192. + + Note that the methods with an _invalid suffix contain flawed regexes and should be + completely discarded. I left them in here for educational purposes, and to remind myself + to think harder and test more thoroughly. It can\'t be that I only found out so late in + the game. For the regex explanation have a look at the forum topic mentioned earlier.'; + + public $loops = 10000; + + public $subjects = array + ( + // Valid colors + 'aaA', + '123', + '000000', + '#123456', + '#abcdef', + + // Invalid colors + 'ggg', + '1234', + '#1234567', + "#000\n", + '}§è!çà%$z', + ); + + // Note that I added the D modifier to corey's regexes. We need to match exactly + // the same if we want the benchmarks to be of any value. + public function bench_corey_regex_1_invalid($subject) + { + return (bool) preg_match('/^#?([0-9a-f]{1,2}){3}$/iD', $subject); + } + + public function bench_corey_regex_2($subject) + { + return (bool) preg_match('/^#?([0-9a-f]){3}(([0-9a-f]){3})?$/iD', $subject); + } + + // Optimized corey_regex_1 + // Using non-capturing parentheses and a possessive interval + public function bench_geert_regex_1a_invalid($subject) + { + return (bool) preg_match('/^#?(?:[0-9a-f]{1,2}+){3}$/iD', $subject); + } + + // Optimized corey_regex_2 + // Removed useless parentheses, made the remaining ones non-capturing + public function bench_geert_regex_2a($subject) + { + return (bool) preg_match('/^#?[0-9a-f]{3}(?:[0-9a-f]{3})?$/iD', $subject); + } + + // Optimized geert_regex_1a + // Possessive "#" + public function bench_geert_regex_1b_invalid($subject) + { + return (bool) preg_match('/^#?+(?:[0-9a-f]{1,2}+){3}$/iD', $subject); + } + + // Optimized geert_regex_2a + // Possessive "#" + public function bench_geert_regex_2b($subject) + { + return (bool) preg_match('/^#?+[0-9a-f]{3}(?:[0-9a-f]{3})?$/iD', $subject); + } + + // Using \z instead of $ + public function bench_salathe_regex_1($subject) + { + return (bool) preg_match('/^#?+[0-9a-f]{3}(?:[0-9a-f]{3})?\z/i', $subject); + } + + // Using \A instead of ^ + public function bench_salathe_regex_2($subject) + { + return (bool) preg_match('/\A#?+[0-9a-f]{3}(?:[0-9a-f]{3})?\z/i', $subject); + } + + // A solution without regex + public function bench_geert_str($subject) + { + if ($subject[0] === '#') + { + $subject = substr($subject, 1); + } + + $strlen = strlen($subject); + return (($strlen === 3 OR $strlen === 6) AND ctype_xdigit($subject)); + } + + // An ugly, but fast, solution without regex + public function bench_salathe_str($subject) + { + if ($subject[0] === '#') + { + $subject = substr($subject, 1); + } + + // TRUE if: + // 1. $subject is 6 or 3 chars long + // 2. $subject contains only hexadecimal digits + return (((isset($subject[5]) AND ! isset($subject[6])) OR + (isset($subject[2]) AND ! isset($subject[3]))) + AND ctype_xdigit($subject)); + } +} \ No newline at end of file diff --git a/includes/kohana/modules/codebench/classes/bench/validurl.php b/includes/kohana/modules/codebench/classes/bench/validurl.php new file mode 100644 index 00000000..3c88675d --- /dev/null +++ b/includes/kohana/modules/codebench/classes/bench/validurl.php @@ -0,0 +1,105 @@ + + */ +class Bench_ValidURL extends Codebench { + + public $description = + 'filter_var vs regex: + http://dev.kohanaframework.org/issues/2847'; + + public $loops = 1000; + + public $subjects = array + ( + // Valid + 'http://google.com', + 'http://google.com/', + 'http://google.com/?q=abc', + 'http://google.com/#hash', + 'http://localhost', + 'http://hello-world.pl', + 'http://hello--world.pl', + 'http://h.e.l.l.0.pl', + 'http://server.tld/get/info', + 'http://127.0.0.1', + 'http://127.0.0.1:80', + 'http://user@127.0.0.1', + 'http://user:pass@127.0.0.1', + 'ftp://my.server.com', + 'rss+xml://rss.example.com', + + // Invalid + 'http://google.2com', + 'http://google.com?q=abc', + 'http://google.com#hash', + 'http://hello-.pl', + 'http://hel.-lo.world.pl', + 'http://ww£.google.com', + 'http://127.0.0.1234', + 'http://127.0.0.1.1', + 'http://user:@127.0.0.1', + "http://finalnewline.com\n", + ); + + public function bench_filter_var($url) + { + return (bool) filter_var($url, FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED); + } + + public function bench_regex($url) + { + // Based on http://www.apps.ietf.org/rfc/rfc1738.html#sec-5 + if ( ! preg_match( + '~^ + + # scheme + [-a-z0-9+.]++:// + + # username:password (optional) + (?: + [-a-z0-9$_.+!*\'(),;?&=%]++ # username + (?::[-a-z0-9$_.+!*\'(),;?&=%]++)? # password (optional) + @ + )? + + (?: + # ip address + \d{1,3}+(?:\.\d{1,3}+){3}+ + + | # or + + # hostname (captured) + ( + (?!-)[-a-z0-9]{1,63}+(? 253) + return FALSE; + + // An extra check for the top level domain + // It must start with a letter + $tld = ltrim(substr($matches[1], (int) strrpos($matches[1], '.')), '.'); + return ctype_alpha($tld[0]); + } + +} \ No newline at end of file diff --git a/includes/kohana/modules/codebench/classes/codebench.php b/includes/kohana/modules/codebench/classes/codebench.php new file mode 100644 index 00000000..0f4c55cd --- /dev/null +++ b/includes/kohana/modules/codebench/classes/codebench.php @@ -0,0 +1,3 @@ +request->redirect('codebench/'.trim($_POST['class'])); + + // Pass the class name on to the view + $this->template->class = (string) $class; + + // Try to load the class, then run it + if (Kohana::auto_load($class) === TRUE) + { + $codebench = new $class; + $this->template->codebench = $codebench->run(); + } + } +} diff --git a/includes/kohana/modules/codebench/classes/kohana/codebench.php b/includes/kohana/modules/codebench/classes/kohana/codebench.php new file mode 100644 index 00000000..d8a71c17 --- /dev/null +++ b/includes/kohana/modules/codebench/classes/kohana/codebench.php @@ -0,0 +1,217 @@ + 'A', + 150 => 'B', + 200 => 'C', + 300 => 'D', + 500 => 'E', + 'default' => 'F', + ); + + /** + * Constructor. + * + * @return void + */ + public function __construct() + { + // Set the maximum execution time + set_time_limit(Kohana::config('codebench')->max_execution_time); + } + + /** + * Runs Codebench on the extending class. + * + * @return array benchmark output + */ + public function run() + { + // Array of all methods to loop over + $methods = array_filter(get_class_methods($this), array($this, '_method_filter')); + + // Make sure the benchmark runs at least once, + // also if no subject data has been provided. + if (empty($this->subjects)) + { + $this->subjects = array('NULL' => NULL); + } + + // Initialize benchmark output + $codebench = array + ( + 'class' => get_class($this), + 'description' => $this->description, + 'loops' => array + ( + 'base' => (int) $this->loops, + 'total' => (int) $this->loops * count($this->subjects) * count($methods), + ), + 'subjects' => $this->subjects, + 'benchmarks' => array(), + ); + + // Benchmark each method + foreach ($methods as $method) + { + // Initialize benchmark output for this method + $codebench['benchmarks'][$method] = array('time' => 0, 'memory' => 0); + + // Using Reflection because simply calling $this->$method($subject) in the loop below + // results in buggy benchmark times correlating to the length of the method name. + $reflection = new ReflectionMethod(get_class($this), $method); + + // Benchmark each subject on each method + foreach ($this->subjects as $subject_key => $subject) + { + // Prerun each method/subject combo before the actual benchmark loop. + // This way relatively expensive initial processes won't be benchmarked, e.g. autoloading. + // At the same time we capture the return here so we don't have to do that in the loop anymore. + $return = $reflection->invoke($this, $subject); + + // Start the timer for one subject + $token = Profiler::start('codebench', $method.$subject_key); + + // The heavy work + for ($i = 0; $i < $this->loops; ++$i) + { + $reflection->invoke($this, $subject); + } + + // Stop and read the timer + $benchmark = Profiler::total($token); + + // Benchmark output specific to the current method and subject + $codebench['benchmarks'][$method]['subjects'][$subject_key] = array + ( + 'return' => $return, + 'time' => $benchmark[0], + 'memory' => $benchmark[1], + ); + + // Update method totals + $codebench['benchmarks'][$method]['time'] += $benchmark[0]; + $codebench['benchmarks'][$method]['memory'] += $benchmark[1]; + } + } + + // Initialize the fastest and slowest benchmarks for both methods and subjects, time and memory, + // these values will be overwritten using min() and max() later on. + // The 999999999 values look like a hack, I know, but they work, + // unless your method runs for more than 31 years or consumes over 1GB of memory. + $fastest_method = $fastest_subject = array('time' => 999999999, 'memory' => 999999999); + $slowest_method = $slowest_subject = array('time' => 0, 'memory' => 0); + + // Find the fastest and slowest benchmarks, needed for the percentage calculations + foreach ($methods as $method) + { + // Update the fastest and slowest method benchmarks + $fastest_method['time'] = min($fastest_method['time'], $codebench['benchmarks'][$method]['time']); + $fastest_method['memory'] = min($fastest_method['memory'], $codebench['benchmarks'][$method]['memory']); + $slowest_method['time'] = max($slowest_method['time'], $codebench['benchmarks'][$method]['time']); + $slowest_method['memory'] = max($slowest_method['memory'], $codebench['benchmarks'][$method]['memory']); + + foreach ($this->subjects as $subject_key => $subject) + { + // Update the fastest and slowest subject benchmarks + $fastest_subject['time'] = min($fastest_subject['time'], $codebench['benchmarks'][$method]['subjects'][$subject_key]['time']); + $fastest_subject['memory'] = min($fastest_subject['memory'], $codebench['benchmarks'][$method]['subjects'][$subject_key]['memory']); + $slowest_subject['time'] = max($slowest_subject['time'], $codebench['benchmarks'][$method]['subjects'][$subject_key]['time']); + $slowest_subject['memory'] = max($slowest_subject['memory'], $codebench['benchmarks'][$method]['subjects'][$subject_key]['memory']); + } + } + + // Percentage calculations for methods + foreach ($codebench['benchmarks'] as & $method) + { + // Calculate percentage difference relative to fastest and slowest methods + $method['percent']['fastest']['time'] = (empty($fastest_method['time'])) ? 0 : $method['time'] / $fastest_method['time'] * 100; + $method['percent']['fastest']['memory'] = (empty($fastest_method['memory'])) ? 0 : $method['memory'] / $fastest_method['memory'] * 100; + $method['percent']['slowest']['time'] = (empty($slowest_method['time'])) ? 0 : $method['time'] / $slowest_method['time'] * 100; + $method['percent']['slowest']['memory'] = (empty($slowest_method['memory'])) ? 0 : $method['memory'] / $slowest_method['memory'] * 100; + + // Assign a grade for time and memory to each method + $method['grade']['time'] = $this->_grade($method['percent']['fastest']['time']); + $method['grade']['memory'] = $this->_grade($method['percent']['fastest']['memory']); + + // Percentage calculations for subjects + foreach ($method['subjects'] as & $subject) + { + // Calculate percentage difference relative to fastest and slowest subjects for this method + $subject['percent']['fastest']['time'] = (empty($fastest_subject['time'])) ? 0 : $subject['time'] / $fastest_subject['time'] * 100; + $subject['percent']['fastest']['memory'] = (empty($fastest_subject['memory'])) ? 0 : $subject['memory'] / $fastest_subject['memory'] * 100; + $subject['percent']['slowest']['time'] = (empty($slowest_subject['time'])) ? 0 : $subject['time'] / $slowest_subject['time'] * 100; + $subject['percent']['slowest']['memory'] = (empty($slowest_subject['memory'])) ? 0 : $subject['memory'] / $slowest_subject['memory'] * 100; + + // Assign a grade letter for time and memory to each subject + $subject['grade']['time'] = $this->_grade($subject['percent']['fastest']['time']); + $subject['grade']['memory'] = $this->_grade($subject['percent']['fastest']['memory']); + } + } + + return $codebench; + } + + /** + * Callback for array_filter(). + * Filters out all methods not to benchmark. + * + * @param string method name + * @return boolean + */ + protected function _method_filter($method) + { + // Only benchmark methods with the "bench" prefix + return (substr($method, 0, 5) === 'bench'); + } + + /** + * Returns the applicable grade letter for a score. + * + * @param integer|double score + * @return string grade letter + */ + protected function _grade($score) + { + foreach ($this->grades as $max => $grade) + { + if ($max === 'default') + continue; + + if ($score <= $max) + return $grade; + } + + return $this->grades['default']; + } +} diff --git a/includes/kohana/modules/codebench/config/codebench.php b/includes/kohana/modules/codebench/config/codebench.php new file mode 100644 index 00000000..590186da --- /dev/null +++ b/includes/kohana/modules/codebench/config/codebench.php @@ -0,0 +1,16 @@ + 0, + + /** + * Expand all benchmark details by default. + */ + 'expand_all' => FALSE, + +); diff --git a/includes/kohana/modules/codebench/init.php b/includes/kohana/modules/codebench/init.php new file mode 100644 index 00000000..866cc341 --- /dev/null +++ b/includes/kohana/modules/codebench/init.php @@ -0,0 +1,8 @@ +)') + ->defaults(array( + 'controller' => 'codebench', + 'action' => 'index', + 'class' => NULL)); diff --git a/includes/kohana/modules/codebench/views/codebench.php b/includes/kohana/modules/codebench/views/codebench.php new file mode 100644 index 00000000..19619503 --- /dev/null +++ b/includes/kohana/modules/codebench/views/codebench.php @@ -0,0 +1,258 @@ + + + + + + + + <?php if ($class !== '') echo $class, ' · ' ?>Codebench + + + + + + + + + + + +
+

+ + + + + Library not found + + No methods found to benchmark + + +

+
+ + + + + +

+ + Remember to prefix the methods you want to benchmark with “bench”.
+ You might also want to overwrite Codebench->method_filter(). +
+

+ + + +
    + $benchmark) { ?> +
  • + +

    + + + +% + +

    + +
    + + + + + + + + + + + + $subject) { ?> + + + + + + + + +
    Benchmarks per subject for
    subject → returns
    + + [] → + + () + + + + + + + + + + + s + + +
    +
    + +
  • + +
+ + + + + + + + Raw output:', Kohana::debug($codebench) ?> + + + + + + + \ No newline at end of file diff --git a/includes/kohana/modules/database/classes/database.php b/includes/kohana/modules/database/classes/database.php new file mode 100644 index 00000000..810f9ed9 --- /dev/null +++ b/includes/kohana/modules/database/classes/database.php @@ -0,0 +1,3 @@ +_database_instance = $config['instance']; + } + + if (isset($config['table'])) + { + $this->_database_table = $config['table']; + } + + parent::__construct(); + } + + /** + * Query the configuration table for all values for this group and + * unserialize each of the values. + * + * @param string group name + * @param array configuration array + * @return $this clone of the current object + */ + public function load($group, array $config = NULL) + { + if ($config === NULL AND $group !== 'database') + { + // Load all of the configuration values for this group + $query = DB::select('config_key', 'config_value') + ->from($this->_database_table) + ->where('group_name', '=', $group) + ->execute($this->_database_instance); + + if (count($query) > 0) + { + // Unserialize the configuration values + $config = array_map('unserialize', $query->as_array('config_key', 'config_value')); + } + } + + return parent::load($group, $config); + } + + /** + * Overload setting offsets to insert or update the database values as + * changes occur. + * + * @param string array key + * @param mixed new value + * @return mixed + */ + public function offsetSet($key, $value) + { + if ( ! $this->offsetExists($key)) + { + // Insert a new value + DB::insert($this->_database_table, array('group_name', 'config_key', 'config_value')) + ->values(array($this->_configuration_group, $key, serialize($value))) + ->execute($this->_database_instance); + } + elseif ($this->offsetGet($key) !== $value) + { + // Update the value + DB::update($this->_database_table) + ->value('config_value', serialize($value)) + ->where('group_name', '=', $this->_configuration_group) + ->where('config_key', '=', $key) + ->execute($this->_database_instance); + } + + return parent::offsetSet($key, $value); + } + +} // End Kohana_Config_Database diff --git a/includes/kohana/modules/database/classes/kohana/database.php b/includes/kohana/modules/database/classes/kohana/database.php new file mode 100644 index 00000000..7c88a32e --- /dev/null +++ b/includes/kohana/modules/database/classes/kohana/database.php @@ -0,0 +1,588 @@ +$name; + } + + if ( ! isset($config['type'])) + { + throw new Kohana_Exception('Database type not defined in :name configuration', + array(':name' => $name)); + } + + // Set the driver class name + $driver = 'Database_'.ucfirst($config['type']); + + // Create the database connection instance + new $driver($name, $config); + } + + return Database::$instances[$name]; + } + + /** + * @var string the last query executed + */ + public $last_query; + + // Character that is used to quote identifiers + protected $_identifier = '"'; + + // Instance name + protected $_instance; + + // Raw server connection + protected $_connection; + + // Configuration array + protected $_config; + + /** + * Stores the database configuration locally and name the instance. + * + * [!!] This method cannot be accessed directly, you must use [Database::instance]. + * + * @return void + */ + protected function __construct($name, array $config) + { + // Set the instance name + $this->_instance = $name; + + // Store the config locally + $this->_config = $config; + + // Store the database instance + Database::$instances[$name] = $this; + } + + /** + * Disconnect from the database when the object is destroyed. + * + * // Destroy the database instance + * unset(Database::instances[(string) $db], $db); + * + * [!!] Calling `unset($db)` is not enough to destroy the database, as it + * will still be stored in `Database::$instances`. + * + * @return void + */ + final public function __destruct() + { + $this->disconnect(); + } + + /** + * Returns the database instance name. + * + * echo (string) $db; + * + * @return string + */ + final public function __toString() + { + return $this->_instance; + } + + /** + * Connect to the database. This is called automatically when the first + * query is executed. + * + * $db->connect(); + * + * @throws Database_Exception + * @return void + */ + abstract public function connect(); + + /** + * Disconnect from the database. This is called automatically by [Database::__destruct]. + * + * $db->disconnect(); + * + * @return boolean + */ + abstract public function disconnect(); + + /** + * Set the connection character set. This is called automatically by [Database::connect]. + * + * $db->set_charset('utf8'); + * + * @throws Database_Exception + * @param string character set name + * @return void + */ + abstract public function set_charset($charset); + + /** + * Perform an SQL query of the given type. + * + * // Make a SELECT query and use objects for results + * $db->query(Database::SELECT, 'SELECT * FROM groups', TRUE); + * + * // Make a SELECT query and use "Model_User" for the results + * $db->query(Database::SELECT, 'SELECT * FROM users LIMIT 1', 'Model_User'); + * + * @param integer Database::SELECT, Database::INSERT, etc + * @param string SQL query + * @param mixed result object class, TRUE for stdClass, FALSE for assoc array + * @return object Database_Result for SELECT queries + * @return array list (insert id, row count) for INSERT queries + * @return integer number of affected rows for all other queries + */ + abstract public function query($type, $sql, $as_object); + + /** + * Count the number of records in the last query, without LIMIT or OFFSET applied. + * + * // Get the total number of records that match the last query + * $count = $db->count_last_query(); + * + * @return integer + */ + public function count_last_query() + { + if ($sql = $this->last_query) + { + $sql = trim($sql); + if (stripos($sql, 'SELECT') !== 0) + { + return FALSE; + } + + if (stripos($sql, 'LIMIT') !== FALSE) + { + // Remove LIMIT from the SQL + $sql = preg_replace('/\sLIMIT\s+[^a-z]+/i', ' ', $sql); + } + + if (stripos($sql, 'OFFSET') !== FALSE) + { + // Remove OFFSET from the SQL + $sql = preg_replace('/\sOFFSET\s+\d+/i', '', $sql); + } + + // Get the total rows from the last query executed + $result = $this->query + ( + Database::SELECT, + 'SELECT COUNT(*) AS '.$this->quote_identifier('total_rows').' '. + 'FROM ('.$sql.') AS '.$this->quote_table('counted_results'), + TRUE + ); + + // Return the total number of rows from the query + return (int) $result->current()->total_rows; + } + + return FALSE; + } + + /** + * Count the number of records in a table. + * + * // Get the total number of records in the "users" table + * $count = $db->count_records('users'); + * + * @param mixed table name string or array(query, alias) + * @return integer + */ + public function count_records($table) + { + // Quote the table name + $table = $this->quote_identifier($table); + + return $this->query(Database::SELECT, 'SELECT COUNT(*) AS total_row_count FROM '.$table, FALSE) + ->get('total_row_count'); + } + + /** + * Returns a normalized array describing the SQL data type + * + * $db->datatype('char'); + * + * @param string SQL data type + * @return array + */ + public function datatype($type) + { + static $types = array + ( + // SQL-92 + 'bit' => array('type' => 'string', 'exact' => TRUE), + 'bit varying' => array('type' => 'string'), + 'char' => array('type' => 'string', 'exact' => TRUE), + 'char varying' => array('type' => 'string'), + 'character' => array('type' => 'string', 'exact' => TRUE), + 'character varying' => array('type' => 'string'), + 'date' => array('type' => 'string'), + 'dec' => array('type' => 'float', 'exact' => TRUE), + 'decimal' => array('type' => 'float', 'exact' => TRUE), + 'double precision' => array('type' => 'float'), + 'float' => array('type' => 'float'), + 'int' => array('type' => 'int', 'min' => '-2147483648', 'max' => '2147483647'), + 'integer' => array('type' => 'int', 'min' => '-2147483648', 'max' => '2147483647'), + 'interval' => array('type' => 'string'), + 'national char' => array('type' => 'string', 'exact' => TRUE), + 'national char varying' => array('type' => 'string'), + 'national character' => array('type' => 'string', 'exact' => TRUE), + 'national character varying' => array('type' => 'string'), + 'nchar' => array('type' => 'string', 'exact' => TRUE), + 'nchar varying' => array('type' => 'string'), + 'numeric' => array('type' => 'float', 'exact' => TRUE), + 'real' => array('type' => 'float'), + 'smallint' => array('type' => 'int', 'min' => '-32768', 'max' => '32767'), + 'time' => array('type' => 'string'), + 'time with time zone' => array('type' => 'string'), + 'timestamp' => array('type' => 'string'), + 'timestamp with time zone' => array('type' => 'string'), + 'varchar' => array('type' => 'string'), + + // SQL:1999 + 'binary large object' => array('type' => 'string', 'binary' => TRUE), + 'blob' => array('type' => 'string', 'binary' => TRUE), + 'boolean' => array('type' => 'bool'), + 'char large object' => array('type' => 'string'), + 'character large object' => array('type' => 'string'), + 'clob' => array('type' => 'string'), + 'national character large object' => array('type' => 'string'), + 'nchar large object' => array('type' => 'string'), + 'nclob' => array('type' => 'string'), + 'time without time zone' => array('type' => 'string'), + 'timestamp without time zone' => array('type' => 'string'), + + // SQL:2003 + 'bigint' => array('type' => 'int', 'min' => '-9223372036854775808', 'max' => '9223372036854775807'), + + // SQL:2008 + 'binary' => array('type' => 'string', 'binary' => TRUE, 'exact' => TRUE), + 'binary varying' => array('type' => 'string', 'binary' => TRUE), + 'varbinary' => array('type' => 'string', 'binary' => TRUE), + ); + + if (isset($types[$type])) + return $types[$type]; + + return array(); + } + + /** + * List all of the tables in the database. Optionally, a LIKE string can + * be used to search for specific tables. + * + * // Get all tables in the current database + * $tables = $db->list_tables(); + * + * // Get all user-related tables + * $tables = $db->list_tables('user%'); + * + * @param string table to search for + * @return array + */ + abstract public function list_tables($like = NULL); + + /** + * Lists all of the columns in a table. Optionally, a LIKE string can be + * used to search for specific fields. + * + * // Get all columns from the "users" table + * $columns = $db->list_columns('users'); + * + * // Get all name-related columns + * $columns = $db->list_columns('users', '%name%'); + * + * @param string table to get columns from + * @param string column to search for + * @return array + */ + abstract public function list_columns($table, $like = NULL); + + /** + * Extracts the text between parentheses, if any. + * + * // Returns: array('CHAR', '6') + * list($type, $length) = $db->_parse_type('CHAR(6)'); + * + * @param string + * @return array list containing the type and length, if any + */ + protected function _parse_type($type) + { + if (($open = strpos($type, '(')) === FALSE) + { + // No length specified + return array($type, NULL); + } + + // Closing parenthesis + $close = strpos($type, ')', $open); + + // Length without parentheses + $length = substr($type, $open + 1, $close - 1 - $open); + + // Type without the length + $type = substr($type, 0, $open).substr($type, $close + 1); + + return array($type, $length); + } + + /** + * Return the table prefix defined in the current configuration. + * + * $prefix = $db->table_prefix(); + * + * @return string + */ + public function table_prefix() + { + return $this->_config['table_prefix']; + } + + /** + * Quote a value for an SQL query. + * + * $db->quote(NULL); // 'NULL' + * $db->quote(10); // 10 + * $db->quote('fred'); // 'fred' + * + * Objects passed to this function will be converted to strings. + * [Database_Expression] objects will use the value of the expression. + * [Database_Query] objects will be compiled and converted to a sub-query. + * All other objects will be converted using the `__toString` method. + * + * @param mixed any value to quote + * @return string + * @uses Database::escape + */ + public function quote($value) + { + if ($value === NULL) + { + return 'NULL'; + } + elseif ($value === TRUE) + { + return "'1'"; + } + elseif ($value === FALSE) + { + return "'0'"; + } + elseif (is_object($value)) + { + if ($value instanceof Database_Query) + { + // Create a sub-query + return '('.$value->compile($this).')'; + } + elseif ($value instanceof Database_Expression) + { + // Use a raw expression + return $value->value(); + } + else + { + // Convert the object to a string + return $this->quote((string) $value); + } + } + elseif (is_array($value)) + { + return '('.implode(', ', array_map(array($this, __FUNCTION__), $value)).')'; + } + elseif (is_int($value)) + { + return (int) $value; + } + elseif (is_float($value)) + { + // Convert to non-locale aware float to prevent possible commas + return sprintf('%F', $value); + } + + return $this->escape($value); + } + + /** + * Quote a database table name and adds the table prefix if needed. + * + * $table = $db->quote_table($table); + * + * @param mixed table name or array(table, alias) + * @return string + * @uses Database::quote_identifier + * @uses Database::table_prefix + */ + public function quote_table($value) + { + // Assign the table by reference from the value + if (is_array($value)) + { + $table =& $value[0]; + + // Attach table prefix to alias + $value[1] = $this->table_prefix().$value[1]; + } + else + { + $table =& $value; + } + + if (is_string($table) AND strpos($table, '.') === FALSE) + { + // Add the table prefix for tables + $table = $this->table_prefix().$table; + } + + return $this->quote_identifier($value); + } + + /** + * Quote a database identifier, such as a column name. Adds the + * table prefix to the identifier if a table name is present. + * + * $column = $db->quote_identifier($column); + * + * You can also use SQL methods within identifiers. + * + * // The value of "column" will be quoted + * $column = $db->quote_identifier('COUNT("column")'); + * + * Objects passed to this function will be converted to strings. + * [Database_Expression] objects will use the value of the expression. + * [Database_Query] objects will be compiled and converted to a sub-query. + * All other objects will be converted using the `__toString` method. + * + * @param mixed any identifier + * @return string + * @uses Database::table_prefix + */ + public function quote_identifier($value) + { + if ($value === '*') + { + return $value; + } + elseif (is_object($value)) + { + if ($value instanceof Database_Query) + { + // Create a sub-query + return '('.$value->compile($this).')'; + } + elseif ($value instanceof Database_Expression) + { + // Use a raw expression + return $value->value(); + } + else + { + // Convert the object to a string + return $this->quote_identifier((string) $value); + } + } + elseif (is_array($value)) + { + // Separate the column and alias + list ($value, $alias) = $value; + + return $this->quote_identifier($value).' AS '.$this->quote_identifier($alias); + } + + if (strpos($value, '"') !== FALSE) + { + // Quote the column in FUNC("ident") identifiers + return preg_replace('/"(.+?)"/e', '$this->quote_identifier("$1")', $value); + } + elseif (strpos($value, '.') !== FALSE) + { + // Split the identifier into the individual parts + $parts = explode('.', $value); + + if ($prefix = $this->table_prefix()) + { + // Get the offset of the table name, 2nd-to-last part + // This works for databases that can have 3 identifiers (Postgre) + $offset = count($parts) - 2; + + // Add the table prefix to the table name + $parts[$offset] = $prefix.$parts[$offset]; + } + + // Quote each of the parts + return implode('.', array_map(array($this, __FUNCTION__), $parts)); + } + else + { + return $this->_identifier.$value.$this->_identifier; + } + } + + /** + * Sanitize a string by escaping characters that could cause an SQL + * injection attack. + * + * $value = $db->escape('any string'); + * + * @param string value to quote + * @return string + */ + abstract public function escape($value); + +} // End Database_Connection diff --git a/includes/kohana/modules/database/classes/kohana/database/exception.php b/includes/kohana/modules/database/classes/kohana/database/exception.php new file mode 100644 index 00000000..ea2630ed --- /dev/null +++ b/includes/kohana/modules/database/classes/kohana/database/exception.php @@ -0,0 +1,11 @@ +_value = $value; + } + + /** + * Get the expression value as a string. + * + * $sql = $expression->value(); + * + * @return string + */ + public function value() + { + return (string) $this->_value; + } + + /** + * Return the value of the expression as a string. + * + * echo $expression; + * + * @return string + * @uses Database_Expression::value + */ + public function __toString() + { + return $this->value(); + } + +} // End Database_Expression diff --git a/includes/kohana/modules/database/classes/kohana/database/mysql.php b/includes/kohana/modules/database/classes/kohana/database/mysql.php new file mode 100644 index 00000000..303c6a33 --- /dev/null +++ b/includes/kohana/modules/database/classes/kohana/database/mysql.php @@ -0,0 +1,381 @@ +_connection) + return; + + if (Database_MySQL::$_set_names === NULL) + { + // Determine if we can use mysql_set_charset(), which is only + // available on PHP 5.2.3+ when compiled against MySQL 5.0+ + Database_MySQL::$_set_names = ! function_exists('mysql_set_charset'); + } + + // Extract the connection parameters, adding required variabels + extract($this->_config['connection'] + array( + 'database' => '', + 'hostname' => '', + 'username' => '', + 'password' => '', + 'persistent' => FALSE, + )); + + // Prevent this information from showing up in traces + unset($this->_config['connection']['username'], $this->_config['connection']['password']); + + try + { + if ($persistent) + { + // Create a persistent connection + $this->_connection = mysql_pconnect($hostname, $username, $password); + } + else + { + // Create a connection and force it to be a new link + $this->_connection = mysql_connect($hostname, $username, $password, TRUE); + } + } + catch (ErrorException $e) + { + // No connection exists + $this->_connection = NULL; + + throw new Database_Exception(':error', array( + ':error' => mysql_error(), + ), + mysql_errno()); + } + + // \xFF is a better delimiter, but the PHP driver uses underscore + $this->_connection_id = sha1($hostname.'_'.$username.'_'.$password); + + $this->_select_db($database); + + if ( ! empty($this->_config['charset'])) + { + // Set the character set + $this->set_charset($this->_config['charset']); + } + } + + /** + * Select the database + * + * @param string Database + * @return void + */ + protected function _select_db($database) + { + if ( ! mysql_select_db($database, $this->_connection)) + { + // Unable to select database + throw new Database_Exception(':error', + array(':error' => mysql_error($this->_connection)), + mysql_errno($this->_connection)); + } + + Database_MySQL::$_current_databases[$this->_connection_id] = $database; + } + + public function disconnect() + { + try + { + // Database is assumed disconnected + $status = TRUE; + + if (is_resource($this->_connection)) + { + if ($status = mysql_close($this->_connection)) + { + // Clear the connection + $this->_connection = NULL; + } + } + } + catch (Exception $e) + { + // Database is probably not disconnected + $status = ! is_resource($this->_connection); + } + + return $status; + } + + public function set_charset($charset) + { + // Make sure the database is connected + $this->_connection or $this->connect(); + + if (Database_MySQL::$_set_names === TRUE) + { + // PHP is compiled against MySQL 4.x + $status = (bool) mysql_query('SET NAMES '.$this->quote($charset), $this->_connection); + } + else + { + // PHP is compiled against MySQL 5.x + $status = mysql_set_charset($charset, $this->_connection); + } + + if ($status === FALSE) + { + throw new Database_Exception(':error', + array(':error' => mysql_error($this->_connection)), + mysql_errno($this->_connection)); + } + } + + public function query($type, $sql, $as_object) + { + // Make sure the database is connected + $this->_connection or $this->connect(); + + if ( ! empty($this->_config['profiling'])) + { + // Benchmark this query for the current instance + $benchmark = Profiler::start("Database ({$this->_instance})", $sql); + } + + if ( ! empty($this->_config['connection']['persistent']) AND $this->_config['connection']['database'] !== Database_MySQL::$_current_databases[$this->_connection_id]) + { + // Select database on persistent connections + $this->_select_db($this->_config['connection']['database']); + } + + // Execute the query + if (($result = mysql_query($sql, $this->_connection)) === FALSE) + { + if (isset($benchmark)) + { + // This benchmark is worthless + Profiler::delete($benchmark); + } + + throw new Database_Exception(':error [ :query ]', + array(':error' => mysql_error($this->_connection), ':query' => $sql), + mysql_errno($this->_connection)); + } + + if (isset($benchmark)) + { + Profiler::stop($benchmark); + } + + // Set the last query + $this->last_query = $sql; + + if ($type === Database::SELECT) + { + // Return an iterator of results + return new Database_MySQL_Result($result, $sql, $as_object); + } + elseif ($type === Database::INSERT) + { + // Return a list of insert id and rows created + return array( + mysql_insert_id($this->_connection), + mysql_affected_rows($this->_connection), + ); + } + else + { + // Return the number of rows affected + return mysql_affected_rows($this->_connection); + } + } + + public function datatype($type) + { + static $types = array + ( + 'blob' => array('type' => 'string', 'binary' => TRUE, 'character_maximum_length' => '65535'), + 'bool' => array('type' => 'bool'), + 'bigint unsigned' => array('type' => 'int', 'min' => '0', 'max' => '18446744073709551615'), + 'datetime' => array('type' => 'string'), + 'decimal unsigned' => array('type' => 'float', 'exact' => TRUE, 'min' => '0'), + 'double' => array('type' => 'float'), + 'double precision unsigned' => array('type' => 'float', 'min' => '0'), + 'double unsigned' => array('type' => 'float', 'min' => '0'), + 'enum' => array('type' => 'string'), + 'fixed' => array('type' => 'float', 'exact' => TRUE), + 'fixed unsigned' => array('type' => 'float', 'exact' => TRUE, 'min' => '0'), + 'float unsigned' => array('type' => 'float', 'min' => '0'), + 'int unsigned' => array('type' => 'int', 'min' => '0', 'max' => '4294967295'), + 'integer unsigned' => array('type' => 'int', 'min' => '0', 'max' => '4294967295'), + 'longblob' => array('type' => 'string', 'binary' => TRUE, 'character_maximum_length' => '4294967295'), + 'longtext' => array('type' => 'string', 'character_maximum_length' => '4294967295'), + 'mediumblob' => array('type' => 'string', 'binary' => TRUE, 'character_maximum_length' => '16777215'), + 'mediumint' => array('type' => 'int', 'min' => '-8388608', 'max' => '8388607'), + 'mediumint unsigned' => array('type' => 'int', 'min' => '0', 'max' => '16777215'), + 'mediumtext' => array('type' => 'string', 'character_maximum_length' => '16777215'), + 'national varchar' => array('type' => 'string'), + 'numeric unsigned' => array('type' => 'float', 'exact' => TRUE, 'min' => '0'), + 'nvarchar' => array('type' => 'string'), + 'point' => array('type' => 'string', 'binary' => TRUE), + 'real unsigned' => array('type' => 'float', 'min' => '0'), + 'set' => array('type' => 'string'), + 'smallint unsigned' => array('type' => 'int', 'min' => '0', 'max' => '65535'), + 'text' => array('type' => 'string', 'character_maximum_length' => '65535'), + 'tinyblob' => array('type' => 'string', 'binary' => TRUE, 'character_maximum_length' => '255'), + 'tinyint' => array('type' => 'int', 'min' => '-128', 'max' => '127'), + 'tinyint unsigned' => array('type' => 'int', 'min' => '0', 'max' => '255'), + 'tinytext' => array('type' => 'string', 'character_maximum_length' => '255'), + 'year' => array('type' => 'string'), + ); + + $type = str_replace(' zerofill', '', $type); + + if (isset($types[$type])) + return $types[$type]; + + return parent::datatype($type); + } + + public function list_tables($like = NULL) + { + if (is_string($like)) + { + // Search for table names + $result = $this->query(Database::SELECT, 'SHOW TABLES LIKE '.$this->quote($like), FALSE); + } + else + { + // Find all table names + $result = $this->query(Database::SELECT, 'SHOW TABLES', FALSE); + } + + $tables = array(); + foreach ($result as $row) + { + $tables[] = reset($row); + } + + return $tables; + } + + public function list_columns($table, $like = NULL) + { + // Quote the table name + $table = $this->quote_table($table); + + if (is_string($like)) + { + // Search for column names + $result = $this->query(Database::SELECT, 'SHOW FULL COLUMNS FROM '.$table.' LIKE '.$this->quote($like), FALSE); + } + else + { + // Find all column names + $result = $this->query(Database::SELECT, 'SHOW FULL COLUMNS FROM '.$table, FALSE); + } + + $count = 0; + $columns = array(); + foreach ($result as $row) + { + list($type, $length) = $this->_parse_type($row['Type']); + + $column = $this->datatype($type); + + $column['column_name'] = $row['Field']; + $column['column_default'] = $row['Default']; + $column['data_type'] = $type; + $column['is_nullable'] = ($row['Null'] == 'YES'); + $column['ordinal_position'] = ++$count; + + switch ($column['type']) + { + case 'float': + if (isset($length)) + { + list($column['numeric_precision'], $column['numeric_scale']) = explode(',', $length); + } + break; + case 'int': + if (isset($length)) + { + // MySQL attribute + $column['display'] = $length; + } + break; + case 'string': + switch ($column['data_type']) + { + case 'binary': + case 'varbinary': + $column['character_maximum_length'] = $length; + break; + + case 'char': + case 'varchar': + $column['character_maximum_length'] = $length; + case 'text': + case 'tinytext': + case 'mediumtext': + case 'longtext': + $column['collation_name'] = $row['Collation']; + break; + + case 'enum': + case 'set': + $column['collation_name'] = $row['Collation']; + $column['options'] = explode('\',\'', substr($length, 1, -1)); + break; + } + break; + } + + // MySQL attributes + $column['comment'] = $row['Comment']; + $column['extra'] = $row['Extra']; + $column['key'] = $row['Key']; + $column['privileges'] = $row['Privileges']; + + $columns[$row['Field']] = $column; + } + + return $columns; + } + + public function escape($value) + { + // Make sure the database is connected + $this->_connection or $this->connect(); + + if (($value = mysql_real_escape_string((string) $value, $this->_connection)) === FALSE) + { + throw new Database_Exception(':error', + array(':error' => mysql_errno($this->_connection)), + mysql_error($this->_connection)); + } + + // SQL standard is to use single-quotes for all values + return "'$value'"; + } + +} // End Database_MySQL diff --git a/includes/kohana/modules/database/classes/kohana/database/mysql/result.php b/includes/kohana/modules/database/classes/kohana/database/mysql/result.php new file mode 100644 index 00000000..8e4751b8 --- /dev/null +++ b/includes/kohana/modules/database/classes/kohana/database/mysql/result.php @@ -0,0 +1,71 @@ +_total_rows = mysql_num_rows($result); + } + + public function __destruct() + { + if (is_resource($this->_result)) + { + mysql_free_result($this->_result); + } + } + + public function seek($offset) + { + if ($this->offsetExists($offset) AND mysql_data_seek($this->_result, $offset)) + { + // Set the current row to the offset + $this->_current_row = $this->_internal_row = $offset; + + return TRUE; + } + else + { + return FALSE; + } + } + + public function current() + { + if ($this->_current_row !== $this->_internal_row AND ! $this->seek($this->_current_row)) + return FALSE; + + // Increment internal row for optimization assuming rows are fetched in order + $this->_internal_row++; + + if ($this->_as_object === TRUE) + { + // Return an stdClass + return mysql_fetch_object($this->_result); + } + elseif (is_string($this->_as_object)) + { + // Return an object of given class name + return mysql_fetch_object($this->_result, $this->_as_object); + } + else + { + // Return an array of the row + return mysql_fetch_assoc($this->_result); + } + } + +} // End Database_MySQL_Result_Select diff --git a/includes/kohana/modules/database/classes/kohana/database/pdo.php b/includes/kohana/modules/database/classes/kohana/database/pdo.php new file mode 100644 index 00000000..227154c8 --- /dev/null +++ b/includes/kohana/modules/database/classes/kohana/database/pdo.php @@ -0,0 +1,186 @@ +_config['identifier'])) + { + // Allow the identifier to be overloaded per-connection + $this->_identifier = (string) $this->_config['identifier']; + } + } + + public function connect() + { + if ($this->_connection) + return; + + // Extract the connection parameters, adding required variabels + extract($this->_config['connection'] + array( + 'dsn' => '', + 'username' => NULL, + 'password' => NULL, + 'persistent' => FALSE, + )); + + // Clear the connection parameters for security + unset($this->_config['connection']); + + // Force PDO to use exceptions for all errors + $attrs = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION); + + if ( ! empty($persistent)) + { + // Make the connection persistent + $attrs[PDO::ATTR_PERSISTENT] = TRUE; + } + + try + { + // Create a new PDO connection + $this->_connection = new PDO($dsn, $username, $password, $attrs); + } + catch (PDOException $e) + { + throw new Database_Exception(':error', array( + ':error' => $e->getMessage(), + ), + $e->getCode(), + $e); + } + + if ( ! empty($this->_config['charset'])) + { + // Set the character set + $this->set_charset($this->_config['charset']); + } + } + + public function disconnect() + { + // Destroy the PDO object + $this->_connection = NULL; + + return TRUE; + } + + public function set_charset($charset) + { + // Make sure the database is connected + $this->_connection or $this->connect(); + + // Execute a raw SET NAMES query + $this->_connection->exec('SET NAMES '.$this->quote($charset)); + } + + public function query($type, $sql, $as_object) + { + // Make sure the database is connected + $this->_connection or $this->connect(); + + if ( ! empty($this->_config['profiling'])) + { + // Benchmark this query for the current instance + $benchmark = Profiler::start("Database ({$this->_instance})", $sql); + } + + try + { + $result = $this->_connection->query($sql); + } + catch (Exception $e) + { + if (isset($benchmark)) + { + // This benchmark is worthless + Profiler::delete($benchmark); + } + + // Convert the exception in a database exception + throw new Database_Exception(':error [ :query ]', array( + ':error' => $e->getMessage(), + ':query' => $sql + ), + $e->getCode(), + $e); + } + + if (isset($benchmark)) + { + Profiler::stop($benchmark); + } + + // Set the last query + $this->last_query = $sql; + + if ($type === Database::SELECT) + { + // Convert the result into an array, as PDOStatement::rowCount is not reliable + if ($as_object === FALSE) + { + $result->setFetchMode(PDO::FETCH_ASSOC); + } + elseif (is_string($as_object)) + { + $result->setFetchMode(PDO::FETCH_CLASS, $as_object); + } + else + { + $result->setFetchMode(PDO::FETCH_CLASS, 'stdClass'); + } + + $result = $result->fetchAll(); + + // Return an iterator of results + return new Database_Result_Cached($result, $sql, $as_object); + } + elseif ($type === Database::INSERT) + { + // Return a list of insert id and rows created + return array( + $this->_connection->lastInsertId(), + $result->rowCount(), + ); + } + else + { + // Return the number of rows affected + return $result->rowCount(); + } + } + + public function list_tables($like = NULL) + { + throw new Kohana_Exception('Database method :method is not supported by :class', + array(':method' => __FUNCTION__, ':class' => __CLASS__)); + } + + public function list_columns($table, $like = NULL) + { + throw new Kohana_Exception('Database method :method is not supported by :class', + array(':method' => __FUNCTION__, ':class' => __CLASS__)); + } + + public function escape($value) + { + // Make sure the database is connected + $this->_connection or $this->connect(); + + return $this->_connection->quote($value); + } + +} // End Database_PDO diff --git a/includes/kohana/modules/database/classes/kohana/database/query.php b/includes/kohana/modules/database/classes/kohana/database/query.php new file mode 100644 index 00000000..d1b3c0c4 --- /dev/null +++ b/includes/kohana/modules/database/classes/kohana/database/query.php @@ -0,0 +1,218 @@ +_type = $type; + $this->_sql = $sql; + } + + /** + * Return the SQL query string. + * + * @return string + */ + final public function __toString() + { + try + { + // Return the SQL string + return $this->compile(Database::instance()); + } + catch (Exception $e) + { + return Kohana::exception_text($e); + } + } + + /** + * Get the type of the query. + * + * @return integer + */ + public function type() + { + return $this->_type; + } + + /** + * Enables the query to be cached for a specified amount of time. + * + * @param integer number of seconds to cache or null for default + * @return $this + */ + public function cached($lifetime = NULL) + { + $this->_lifetime = $lifetime; + + return $this; + } + + /** + * Returns results as associative arrays + * + * @return $this + */ + public function as_assoc() + { + $this->_as_object = FALSE; + + return $this; + } + + /** + * Returns results as objects + * + * @param string classname or TRUE for stdClass + * @return $this + */ + public function as_object($class = TRUE) + { + $this->_as_object = $class; + + return $this; + } + + /** + * Set the value of a parameter in the query. + * + * @param string parameter key to replace + * @param mixed value to use + * @return $this + */ + public function param($param, $value) + { + // Add or overload a new parameter + $this->_parameters[$param] = $value; + + return $this; + } + + /** + * Bind a variable to a parameter in the query. + * + * @param string parameter key to replace + * @param mixed variable to use + * @return $this + */ + public function bind($param, & $var) + { + // Bind a value to a variable + $this->_parameters[$param] =& $var; + + return $this; + } + + /** + * Add multiple parameters to the query. + * + * @param array list of parameters + * @return $this + */ + public function parameters(array $params) + { + // Merge the new parameters in + $this->_parameters = $params + $this->_parameters; + + return $this; + } + + /** + * Compile the SQL query and return it. Replaces any parameters with their + * given values. + * + * @param object Database instance + * @return string + */ + public function compile(Database $db) + { + // Import the SQL locally + $sql = $this->_sql; + + if ( ! empty($this->_parameters)) + { + // Quote all of the values + $values = array_map(array($db, 'quote'), $this->_parameters); + + // Replace the values in the SQL + $sql = strtr($sql, $values); + } + + return $sql; + } + + /** + * Execute the current query on the given database. + * + * @param mixed Database instance or name of instance + * @return object Database_Result for SELECT queries + * @return mixed the insert id for INSERT queries + * @return integer number of affected rows for all other queries + */ + public function execute($db = NULL) + { + if ( ! is_object($db)) + { + // Get the database instance + $db = Database::instance($db); + } + + // Compile the SQL query + $sql = $this->compile($db); + + if ( ! empty($this->_lifetime) AND $this->_type === Database::SELECT) + { + // Set the cache key based on the database instance name and SQL + $cache_key = 'Database::query("'.$db.'", "'.$sql.'")'; + + if ($result = Kohana::cache($cache_key, NULL, $this->_lifetime)) + { + // Return a cached result + return new Database_Result_Cached($result, $sql, $this->_as_object); + } + } + + // Execute the query + $result = $db->query($this->_type, $sql, $this->_as_object); + + if (isset($cache_key)) + { + // Cache the result array + Kohana::cache($cache_key, $result->as_array(), $this->_lifetime); + } + + return $result; + } + +} // End Database_Query diff --git a/includes/kohana/modules/database/classes/kohana/database/query/builder.php b/includes/kohana/modules/database/classes/kohana/database/query/builder.php new file mode 100644 index 00000000..9cf4ec61 --- /dev/null +++ b/includes/kohana/modules/database/classes/kohana/database/query/builder.php @@ -0,0 +1,199 @@ +compile($db); + } + + return implode(' ', $statements); + } + + /** + * Compiles an array of conditions into an SQL partial. Used for WHERE + * and HAVING. + * + * @param object Database instance + * @param array condition statements + * @return string + */ + protected function _compile_conditions(Database $db, array $conditions) + { + $last_condition = NULL; + + $sql = ''; + foreach ($conditions as $group) + { + // Process groups of conditions + foreach ($group as $logic => $condition) + { + if ($condition === '(') + { + if ( ! empty($sql) AND $last_condition !== '(') + { + // Include logic operator + $sql .= ' '.$logic.' '; + } + + $sql .= '('; + } + elseif ($condition === ')') + { + $sql .= ')'; + } + else + { + if ( ! empty($sql) AND $last_condition !== '(') + { + // Add the logic operator + $sql .= ' '.$logic.' '; + } + + // Split the condition + list($column, $op, $value) = $condition; + + if ($value === NULL) + { + if ($op === '=') + { + // Convert "val = NULL" to "val IS NULL" + $op = 'IS'; + } + elseif ($op === '!=') + { + // Convert "val != NULL" to "valu IS NOT NULL" + $op = 'IS NOT'; + } + } + + // Database operators are always uppercase + $op = strtoupper($op); + + if ($op === 'BETWEEN' AND is_array($value)) + { + // BETWEEN always has exactly two arguments + list($min, $max) = $value; + + if (is_string($min) AND array_key_exists($min, $this->_parameters)) + { + // Set the parameter as the minimum + $min = $this->_parameters[$min]; + } + + if (is_string($max) AND array_key_exists($max, $this->_parameters)) + { + // Set the parameter as the maximum + $max = $this->_parameters[$max]; + } + + // Quote the min and max value + $value = $db->quote($min).' AND '.$db->quote($max); + } + else + { + if (is_string($value) AND array_key_exists($value, $this->_parameters)) + { + // Set the parameter as the value + $value = $this->_parameters[$value]; + } + + // Quote the entire value normally + $value = $db->quote($value); + } + + // Append the statement to the query + $sql .= $db->quote_identifier($column).' '.$op.' '.$value; + } + + $last_condition = $condition; + } + } + + return $sql; + } + + /** + * Compiles an array of set values into an SQL partial. Used for UPDATE. + * + * @param object Database instance + * @param array updated values + * @return string + */ + protected function _compile_set(Database $db, array $values) + { + $set = array(); + foreach ($values as $group) + { + // Split the set + list ($column, $value) = $group; + + // Quote the column name + $column = $db->quote_identifier($column); + + if (is_string($value) AND array_key_exists($value, $this->_parameters)) + { + // Use the parameter value + $value = $this->_parameters[$value]; + } + + $set[$column] = $column.' = '.$db->quote($value); + } + + return implode(', ', $set); + } + + /** + * Compiles an array of ORDER BY statements into an SQL partial. + * + * @param object Database instance + * @param array sorting columns + * @return string + */ + protected function _compile_order_by(Database $db, array $columns) + { + $sort = array(); + foreach ($columns as $group) + { + list ($column, $direction) = $group; + + if ( ! empty($direction)) + { + // Make the direction uppercase + $direction = ' '.strtoupper($direction); + } + + $sort[] = $db->quote_identifier($column).$direction; + } + + return 'ORDER BY '.implode(', ', $sort); + } + + /** + * Reset the current builder status. + * + * @return $this + */ + abstract public function reset(); + +} // End Database_Query_Builder diff --git a/includes/kohana/modules/database/classes/kohana/database/query/builder/delete.php b/includes/kohana/modules/database/classes/kohana/database/query/builder/delete.php new file mode 100644 index 00000000..fffa37c5 --- /dev/null +++ b/includes/kohana/modules/database/classes/kohana/database/query/builder/delete.php @@ -0,0 +1,89 @@ +_table = $table; + } + + // Start the query with no SQL + return parent::__construct(Database::DELETE, ''); + } + + /** + * Sets the table to delete from. + * + * @param mixed table name or array($table, $alias) or object + * @return $this + */ + public function table($table) + { + $this->_table = $table; + + return $this; + } + + /** + * Compile the SQL query and return it. + * + * @param object Database instance + * @return string + */ + public function compile(Database $db) + { + // Start a deletion query + $query = 'DELETE FROM '.$db->quote_table($this->_table); + + if ( ! empty($this->_where)) + { + // Add deletion conditions + $query .= ' WHERE '.$this->_compile_conditions($db, $this->_where); + } + + if ( ! empty($this->_order_by)) + { + // Add sorting + $query .= ' '.$this->_compile_order_by($db, $this->_order_by); + } + + if ($this->_limit !== NULL) + { + // Add limiting + $query .= ' LIMIT '.$this->_limit; + } + + return $query; + } + + public function reset() + { + $this->_table = NULL; + $this->_where = array(); + + $this->_parameters = array(); + + return $this; + } + +} // End Database_Query_Builder_Delete diff --git a/includes/kohana/modules/database/classes/kohana/database/query/builder/insert.php b/includes/kohana/modules/database/classes/kohana/database/query/builder/insert.php new file mode 100644 index 00000000..007c530c --- /dev/null +++ b/includes/kohana/modules/database/classes/kohana/database/query/builder/insert.php @@ -0,0 +1,171 @@ +_table = $table; + } + + if ($columns) + { + // Set the column names + $this->_columns = $columns; + } + + // Start the query with no SQL + return parent::__construct(Database::INSERT, ''); + } + + /** + * Sets the table to insert into. + * + * @param mixed table name or array($table, $alias) or object + * @return $this + */ + public function table($table) + { + $this->_table = $table; + + return $this; + } + + /** + * Set the columns that will be inserted. + * + * @param array column names + * @return $this + */ + public function columns(array $columns) + { + $this->_columns = $columns; + + return $this; + } + + /** + * Adds or overwrites values. Multiple value sets can be added. + * + * @param array values list + * @param ... + * @return $this + */ + public function values(array $values) + { + if ( ! is_array($this->_values)) + { + throw new Kohana_Exception('INSERT INTO ... SELECT statements cannot be combined with INSERT INTO ... VALUES'); + } + + // Get all of the passed values + $values = func_get_args(); + + $this->_values = array_merge($this->_values, $values); + + return $this; + } + + /** + * Use a sub-query to for the inserted values. + * + * @param object Database_Query of SELECT type + * @return $this + */ + public function select(Database_Query $query) + { + if ($query->type() !== Database::SELECT) + { + throw new Kohana_Exception('Only SELECT queries can be combined with INSERT queries'); + } + + $this->_values = $query; + + return $this; + } + + /** + * Compile the SQL query and return it. + * + * @param object Database instance + * @return string + */ + public function compile(Database $db) + { + // Start an insertion query + $query = 'INSERT INTO '.$db->quote_table($this->_table); + + // Add the column names + $query .= ' ('.implode(', ', array_map(array($db, 'quote_identifier'), $this->_columns)).') '; + + if (is_array($this->_values)) + { + // Callback for quoting values + $quote = array($db, 'quote'); + + $groups = array(); + foreach ($this->_values as $group) + { + foreach ($group as $i => $value) + { + if (is_string($value) AND isset($this->_parameters[$value])) + { + // Use the parameter value + $group[$i] = $this->_parameters[$value]; + } + } + + $groups[] = '('.implode(', ', array_map($quote, $group)).')'; + } + + // Add the values + $query .= 'VALUES '.implode(', ', $groups); + } + else + { + // Add the sub-query + $query .= (string) $this->_values; + } + + return $query; + } + + public function reset() + { + $this->_table = NULL; + + $this->_columns = + $this->_values = array(); + + $this->_parameters = array(); + + return $this; + } + +} // End Database_Query_Builder_Insert diff --git a/includes/kohana/modules/database/classes/kohana/database/query/builder/join.php b/includes/kohana/modules/database/classes/kohana/database/query/builder/join.php new file mode 100644 index 00000000..3d2c2f2d --- /dev/null +++ b/includes/kohana/modules/database/classes/kohana/database/query/builder/join.php @@ -0,0 +1,107 @@ +_table = $table; + + if ($type !== NULL) + { + // Set the JOIN type + $this->_type = (string) $type; + } + } + + /** + * Adds a new condition for joining. + * + * @param mixed column name or array($column, $alias) or object + * @param string logic operator + * @param mixed column name or array($column, $alias) or object + * @return $this + */ + public function on($c1, $op, $c2) + { + $this->_on[] = array($c1, $op, $c2); + + return $this; + } + + /** + * Compile the SQL partial for a JOIN statement and return it. + * + * @param object Database instance + * @return string + */ + public function compile(Database $db) + { + if ($this->_type) + { + $sql = strtoupper($this->_type).' JOIN'; + } + else + { + $sql = 'JOIN'; + } + + // Quote the table name that is being joined + $sql .= ' '.$db->quote_table($this->_table).' ON '; + + $conditions = array(); + foreach ($this->_on as $condition) + { + // Split the condition + list($c1, $op, $c2) = $condition; + + if ($op) + { + // Make the operator uppercase and spaced + $op = ' '.strtoupper($op); + } + + // Quote each of the identifiers used for the condition + $conditions[] = $db->quote_identifier($c1).$op.' '.$db->quote_identifier($c2); + } + + // Concat the conditions "... AND ..." + $sql .= '('.implode(' AND ', $conditions).')'; + + return $sql; + } + + public function reset() + { + $this->_type = + $this->_table = NULL; + + $this->_on = array(); + } + +} // End Database_Query_Builder_Join diff --git a/includes/kohana/modules/database/classes/kohana/database/query/builder/select.php b/includes/kohana/modules/database/classes/kohana/database/query/builder/select.php new file mode 100644 index 00000000..d26d2c52 --- /dev/null +++ b/includes/kohana/modules/database/classes/kohana/database/query/builder/select.php @@ -0,0 +1,388 @@ +_select = $columns; + } + + // Start the query with no actual SQL statement + parent::__construct(Database::SELECT, ''); + } + + /** + * Enables or disables selecting only unique columns using "SELECT DISTINCT" + * + * @param boolean enable or disable distinct columns + * @return $this + */ + public function distinct($value) + { + $this->_distinct = (bool) $value; + + return $this; + } + + /** + * Choose the columns to select from. + * + * @param mixed column name or array($column, $alias) or object + * @param ... + * @return $this + */ + public function select($columns = NULL) + { + $columns = func_get_args(); + + $this->_select = array_merge($this->_select, $columns); + + return $this; + } + + /** + * Choose the columns to select from, using an array. + * + * @param array list of column names or aliases + * @return $this + */ + public function select_array(array $columns) + { + $this->_select = array_merge($this->_select, $columns); + + return $this; + } + + /** + * Choose the tables to select "FROM ..." + * + * @param mixed table name or array($table, $alias) or object + * @param ... + * @return $this + */ + public function from($tables) + { + $tables = func_get_args(); + + $this->_from = array_merge($this->_from, $tables); + + return $this; + } + + /** + * Adds addition tables to "JOIN ...". + * + * @param mixed column name or array($column, $alias) or object + * @param string join type (LEFT, RIGHT, INNER, etc) + * @return $this + */ + public function join($table, $type = NULL) + { + $this->_join[] = $this->_last_join = new Database_Query_Builder_Join($table, $type); + + return $this; + } + + /** + * Adds "ON ..." conditions for the last created JOIN statement. + * + * @param mixed column name or array($column, $alias) or object + * @param string logic operator + * @param mixed column name or array($column, $alias) or object + * @return $this + */ + public function on($c1, $op, $c2) + { + $this->_last_join->on($c1, $op, $c2); + + return $this; + } + + /** + * Creates a "GROUP BY ..." filter. + * + * @param mixed column name or array($column, $alias) or object + * @param ... + * @return $this + */ + public function group_by($columns) + { + $columns = func_get_args(); + + $this->_group_by = array_merge($this->_group_by, $columns); + + return $this; + } + + /** + * Alias of and_having() + * + * @param mixed column name or array($column, $alias) or object + * @param string logic operator + * @param mixed column value + * @return $this + */ + public function having($column, $op, $value = NULL) + { + return $this->and_having($column, $op, $value); + } + + /** + * Creates a new "AND HAVING" condition for the query. + * + * @param mixed column name or array($column, $alias) or object + * @param string logic operator + * @param mixed column value + * @return $this + */ + public function and_having($column, $op, $value = NULL) + { + $this->_having[] = array('AND' => array($column, $op, $value)); + + return $this; + } + + /** + * Creates a new "OR HAVING" condition for the query. + * + * @param mixed column name or array($column, $alias) or object + * @param string logic operator + * @param mixed column value + * @return $this + */ + public function or_having($column, $op, $value = NULL) + { + $this->_having[] = array('OR' => array($column, $op, $value)); + + return $this; + } + + /** + * Alias of and_having_open() + * + * @return $this + */ + public function having_open() + { + return $this->and_having_open(); + } + + /** + * Opens a new "AND HAVING (...)" grouping. + * + * @return $this + */ + public function and_having_open() + { + $this->_having[] = array('AND' => '('); + + return $this; + } + + /** + * Opens a new "OR HAVING (...)" grouping. + * + * @return $this + */ + public function or_having_open() + { + $this->_having[] = array('OR' => '('); + + return $this; + } + + /** + * Closes an open "AND HAVING (...)" grouping. + * + * @return $this + */ + public function having_close() + { + return $this->and_having_close(); + } + + /** + * Closes an open "AND HAVING (...)" grouping. + * + * @return $this + */ + public function and_having_close() + { + $this->_having[] = array('AND' => ')'); + + return $this; + } + + /** + * Closes an open "OR HAVING (...)" grouping. + * + * @return $this + */ + public function or_having_close() + { + $this->_having[] = array('OR' => ')'); + + return $this; + } + + /** + * Start returning results after "OFFSET ..." + * + * @param integer starting result number + * @return $this + */ + public function offset($number) + { + $this->_offset = (int) $number; + + return $this; + } + + /** + * Compile the SQL query and return it. + * + * @param object Database instance + * @return string + */ + public function compile(Database $db) + { + // Callback to quote identifiers + $quote_ident = array($db, 'quote_identifier'); + + // Callback to quote tables + $quote_table = array($db, 'quote_table'); + + // Start a selection query + $query = 'SELECT '; + + if ($this->_distinct === TRUE) + { + // Select only unique results + $query .= 'DISTINCT '; + } + + if (empty($this->_select)) + { + // Select all columns + $query .= '*'; + } + else + { + // Select all columns + $query .= implode(', ', array_unique(array_map($quote_ident, $this->_select))); + } + + if ( ! empty($this->_from)) + { + // Set tables to select from + $query .= ' FROM '.implode(', ', array_unique(array_map($quote_table, $this->_from))); + } + + if ( ! empty($this->_join)) + { + // Add tables to join + $query .= ' '.$this->_compile_join($db, $this->_join); + } + + if ( ! empty($this->_where)) + { + // Add selection conditions + $query .= ' WHERE '.$this->_compile_conditions($db, $this->_where); + } + + if ( ! empty($this->_group_by)) + { + // Add sorting + $query .= ' GROUP BY '.implode(', ', array_map($quote_ident, $this->_group_by)); + } + + if ( ! empty($this->_having)) + { + // Add filtering conditions + $query .= ' HAVING '.$this->_compile_conditions($db, $this->_having); + } + + if ( ! empty($this->_order_by)) + { + // Add sorting + $query .= ' '.$this->_compile_order_by($db, $this->_order_by); + } + + if ($this->_limit !== NULL) + { + // Add limiting + $query .= ' LIMIT '.$this->_limit; + } + + if ($this->_offset !== NULL) + { + // Add offsets + $query .= ' OFFSET '.$this->_offset; + } + + return $query; + } + + public function reset() + { + $this->_select = + $this->_from = + $this->_join = + $this->_where = + $this->_group_by = + $this->_having = + $this->_order_by = array(); + + $this->_distinct = FALSE; + + $this->_limit = + $this->_offset = + $this->_last_join = NULL; + + $this->_parameters = array(); + + return $this; + } + +} // End Database_Query_Select diff --git a/includes/kohana/modules/database/classes/kohana/database/query/builder/update.php b/includes/kohana/modules/database/classes/kohana/database/query/builder/update.php new file mode 100644 index 00000000..863b5c70 --- /dev/null +++ b/includes/kohana/modules/database/classes/kohana/database/query/builder/update.php @@ -0,0 +1,124 @@ +_table = $table; + } + + // Start the query with no SQL + return parent::__construct(Database::UPDATE, ''); + } + + /** + * Sets the table to update. + * + * @param mixed table name or array($table, $alias) or object + * @return $this + */ + public function table($table) + { + $this->_table = $table; + + return $this; + } + + /** + * Set the values to update with an associative array. + * + * @param array associative (column => value) list + * @return $this + */ + public function set(array $pairs) + { + foreach ($pairs as $column => $value) + { + $this->_set[] = array($column, $value); + } + + return $this; + } + + /** + * Set the value of a single column. + * + * @param mixed table name or array($table, $alias) or object + * @param mixed column value + * @return $this + */ + public function value($column, $value) + { + $this->_set[] = array($column, $value); + + return $this; + } + + /** + * Compile the SQL query and return it. + * + * @param object Database instance + * @return string + */ + public function compile(Database $db) + { + // Start an update query + $query = 'UPDATE '.$db->quote_table($this->_table); + + // Add the columns to update + $query .= ' SET '.$this->_compile_set($db, $this->_set); + + if ( ! empty($this->_where)) + { + // Add selection conditions + $query .= ' WHERE '.$this->_compile_conditions($db, $this->_where); + } + + if ($this->_limit !== NULL) + { + // Add limiting + $query .= ' LIMIT '.$this->_limit; + } + + return $query; + } + + public function reset() + { + $this->_table = NULL; + + $this->_set = + $this->_where = array(); + + $this->_limit = NULL; + + $this->_parameters = array(); + + return $this; + } + + +} // End Database_Query_Builder_Update diff --git a/includes/kohana/modules/database/classes/kohana/database/query/builder/where.php b/includes/kohana/modules/database/classes/kohana/database/query/builder/where.php new file mode 100644 index 00000000..5b8e0236 --- /dev/null +++ b/includes/kohana/modules/database/classes/kohana/database/query/builder/where.php @@ -0,0 +1,160 @@ +and_where($column, $op, $value); + } + + /** + * Creates a new "AND WHERE" condition for the query. + * + * @param mixed column name or array($column, $alias) or object + * @param string logic operator + * @param mixed column value + * @return $this + */ + public function and_where($column, $op, $value) + { + $this->_where[] = array('AND' => array($column, $op, $value)); + + return $this; + } + + /** + * Creates a new "OR WHERE" condition for the query. + * + * @param mixed column name or array($column, $alias) or object + * @param string logic operator + * @param mixed column value + * @return $this + */ + public function or_where($column, $op, $value) + { + $this->_where[] = array('OR' => array($column, $op, $value)); + + return $this; + } + + /** + * Alias of and_where_open() + * + * @return $this + */ + public function where_open() + { + return $this->and_where_open(); + } + + /** + * Opens a new "AND WHERE (...)" grouping. + * + * @return $this + */ + public function and_where_open() + { + $this->_where[] = array('AND' => '('); + + return $this; + } + + /** + * Opens a new "OR WHERE (...)" grouping. + * + * @return $this + */ + public function or_where_open() + { + $this->_where[] = array('OR' => '('); + + return $this; + } + + /** + * Closes an open "AND WHERE (...)" grouping. + * + * @return $this + */ + public function where_close() + { + return $this->and_where_close(); + } + + /** + * Closes an open "AND WHERE (...)" grouping. + * + * @return $this + */ + public function and_where_close() + { + $this->_where[] = array('AND' => ')'); + + return $this; + } + + /** + * Closes an open "OR WHERE (...)" grouping. + * + * @return $this + */ + public function or_where_close() + { + $this->_where[] = array('OR' => ')'); + + return $this; + } + + /** + * Applies sorting with "ORDER BY ..." + * + * @param mixed column name or array($column, $alias) or object + * @param string direction of sorting + * @return $this + */ + public function order_by($column, $direction = NULL) + { + $this->_order_by[] = array($column, $direction); + + return $this; + } + + /** + * Return up to "LIMIT ..." results + * + * @param integer maximum results to return + * @return $this + */ + public function limit($number) + { + $this->_limit = (int) $number; + + return $this; + } + +} // End Database_Query_Builder_Where diff --git a/includes/kohana/modules/database/classes/kohana/database/result.php b/includes/kohana/modules/database/classes/kohana/database/result.php new file mode 100644 index 00000000..c3d4fca5 --- /dev/null +++ b/includes/kohana/modules/database/classes/kohana/database/result.php @@ -0,0 +1,322 @@ +_result = $result; + + // Store the SQL locally + $this->_query = $sql; + + if (is_object($as_object)) + { + // Get the object class name + $as_object = get_class($as_object); + } + + // Results as objects or associative arrays + $this->_as_object = $as_object; + } + + /** + * Result destruction cleans up all open result sets. + * + * @return void + */ + abstract public function __destruct(); + + /** + * Get a cached database result from the current result iterator. + * + * $cachable = serialize($result->cached()); + * + * @return Database_Result_Cached + * @since 3.0.5 + */ + public function cached() + { + return new Database_Result_Cached($this->as_array(), $this->_query, $this->_as_object); + } + + /** + * Return all of the rows in the result as an array. + * + * // Indexed array of all rows + * $rows = $result->as_array(); + * + * // Associative array of rows by "id" + * $rows = $result->as_array('id'); + * + * // Associative array of rows, "id" => "name" + * $rows = $result->as_array('id', 'name'); + * + * @param string column for associative keys + * @param string column for values + * @return array + */ + public function as_array($key = NULL, $value = NULL) + { + $results = array(); + + if ($key === NULL AND $value === NULL) + { + // Indexed rows + + foreach ($this as $row) + { + $results[] = $row; + } + } + elseif ($key === NULL) + { + // Indexed columns + + if ($this->_as_object) + { + foreach ($this as $row) + { + $results[] = $row->$value; + } + } + else + { + foreach ($this as $row) + { + $results[] = $row[$value]; + } + } + } + elseif ($value === NULL) + { + // Associative rows + + if ($this->_as_object) + { + foreach ($this as $row) + { + $results[$row->$key] = $row; + } + } + else + { + foreach ($this as $row) + { + $results[$row[$key]] = $row; + } + } + } + else + { + // Associative columns + + if ($this->_as_object) + { + foreach ($this as $row) + { + $results[$row->$key] = $row->$value; + } + } + else + { + foreach ($this as $row) + { + $results[$row[$key]] = $row[$value]; + } + } + } + + $this->rewind(); + + return $results; + } + + /** + * Return the named column from the current row. + * + * // Get the "id" value + * $id = $result->get('id'); + * + * @param string column to get + * @param mixed default value if the column does not exist + * @return mixed + */ + public function get($name, $default = NULL) + { + $row = $this->current(); + + if ($this->_as_object) + { + if (isset($row->$name)) + return $row->$name; + } + else + { + if (isset($row[$name])) + return $row[$name]; + } + + return $default; + } + + /** + * Implements [Countable::count], returns the total number of rows. + * + * echo count($result); + * + * @return integer + */ + public function count() + { + return $this->_total_rows; + } + + /** + * Implements [ArrayAccess::offsetExists], determines if row exists. + * + * if (isset($result[10])) + * { + * // Row 10 exists + * } + * + * @return boolean + */ + public function offsetExists($offset) + { + return ($offset >= 0 AND $offset < $this->_total_rows); + } + + /** + * Implements [ArrayAccess::offsetGet], gets a given row. + * + * $row = $result[10]; + * + * @return mixed + */ + public function offsetGet($offset) + { + if ( ! $this->seek($offset)) + return NULL; + + return $this->current(); + } + + /** + * Implements [ArrayAccess::offsetSet], throws an error. + * + * [!!] You cannot modify a database result. + * + * @return void + * @throws Kohana_Exception + */ + final public function offsetSet($offset, $value) + { + throw new Kohana_Exception('Database results are read-only'); + } + + /** + * Implements [ArrayAccess::offsetUnset], throws an error. + * + * [!!] You cannot modify a database result. + * + * @return void + * @throws Kohana_Exception + */ + final public function offsetUnset($offset) + { + throw new Kohana_Exception('Database results are read-only'); + } + + /** + * Implements [Iterator::key], returns the current row number. + * + * echo key($result); + * + * @return integer + */ + public function key() + { + return $this->_current_row; + } + + /** + * Implements [Iterator::next], moves to the next row. + * + * next($result); + * + * @return $this + */ + public function next() + { + ++$this->_current_row; + return $this; + } + + /** + * Implements [Iterator::prev], moves to the previous row. + * + * prev($result); + * + * @return $this + */ + public function prev() + { + --$this->_current_row; + return $this; + } + + /** + * Implements [Iterator::rewind], sets the current row to zero. + * + * rewind($result); + * + * @return $this + */ + public function rewind() + { + $this->_current_row = 0; + return $this; + } + + /** + * Implements [Iterator::valid], checks if the current row exists. + * + * [!!] This method is only used internally. + * + * @return boolean + */ + public function valid() + { + return $this->offsetExists($this->_current_row); + } + +} // End Database_Result diff --git a/includes/kohana/modules/database/classes/kohana/database/result/cached.php b/includes/kohana/modules/database/classes/kohana/database/result/cached.php new file mode 100644 index 00000000..852f5b5e --- /dev/null +++ b/includes/kohana/modules/database/classes/kohana/database/result/cached.php @@ -0,0 +1,51 @@ +_total_rows = count($result); + } + + public function __destruct() + { + // Cached results do not use resources + } + + public function cached() + { + return $this; + } + + public function seek($offset) + { + if ($this->offsetExists($offset)) + { + $this->_current_row = $offset; + + return TRUE; + } + else + { + return FALSE; + } + } + + public function current() + { + // Return an array of the row + return $this->_result[$this->_current_row]; + } + +} // End Database_Result_Cached diff --git a/includes/kohana/modules/database/classes/kohana/db.php b/includes/kohana/modules/database/classes/kohana/db.php new file mode 100644 index 00000000..189e4462 --- /dev/null +++ b/includes/kohana/modules/database/classes/kohana/db.php @@ -0,0 +1,126 @@ + 'session_id', + 'last_active' => 'last_active', + 'contents' => 'contents' + ); + + // Garbage collection requests + protected $_gc = 500; + + // The current session id + protected $_session_id; + + // The old session id + protected $_update_id; + + public function __construct(array $config = NULL, $id = NULL) + { + if ( ! isset($config['group'])) + { + // Use the default group + $config['group'] = 'default'; + } + + // Load the database + $this->_db = Database::instance($config['group']); + + if (isset($config['table'])) + { + // Set the table name + $this->_table = (string) $config['table']; + } + + if (isset($config['gc'])) + { + // Set the gc chance + $this->_gc = (int) $config['gc']; + } + + if (isset($config['columns'])) + { + // Overload column names + $this->_columns = $config['columns']; + } + + parent::__construct($config, $id); + + if (mt_rand(0, $this->_gc) === $this->_gc) + { + // Run garbage collection + // This will average out to run once every X requests + $this->_gc(); + } + } + + public function id() + { + return $this->_session_id; + } + + protected function _read($id = NULL) + { + if ($id OR $id = Cookie::get($this->_name)) + { + $result = DB::select(array($this->_columns['contents'], 'contents')) + ->from($this->_table) + ->where($this->_columns['session_id'], '=', ':id') + ->limit(1) + ->param(':id', $id) + ->execute($this->_db); + + if ($result->count()) + { + // Set the current session id + $this->_session_id = $this->_update_id = $id; + + // Return the contents + return $result->get('contents'); + } + } + + // Create a new session id + $this->_regenerate(); + + return NULL; + } + + protected function _regenerate() + { + // Create the query to find an ID + $query = DB::select($this->_columns['session_id']) + ->from($this->_table) + ->where($this->_columns['session_id'], '=', ':id') + ->limit(1) + ->bind(':id', $id); + + do + { + // Create a new session id + $id = str_replace('.', '-', uniqid(NULL, TRUE)); + + // Get the the id from the database + $result = $query->execute($this->_db); + } + while ($result->count()); + + return $this->_session_id = $id; + } + + protected function _write() + { + if ($this->_update_id === NULL) + { + // Insert a new row + $query = DB::insert($this->_table, $this->_columns) + ->values(array(':new_id', ':active', ':contents')); + } + else + { + // Update the row + $query = DB::update($this->_table) + ->value($this->_columns['last_active'], ':active') + ->value($this->_columns['contents'], ':contents') + ->where($this->_columns['session_id'], '=', ':old_id'); + + if ($this->_update_id !== $this->_session_id) + { + // Also update the session id + $query->value($this->_columns['session_id'], ':new_id'); + } + } + + $query + ->param(':new_id', $this->_session_id) + ->param(':old_id', $this->_update_id) + ->param(':active', $this->_data['last_active']) + ->param(':contents', $this->__toString()); + + // Execute the query + $query->execute($this->_db); + + // The update and the session id are now the same + $this->_update_id = $this->_session_id; + + // Update the cookie with the new session id + Cookie::set($this->_name, $this->_session_id, $this->_lifetime); + + return TRUE; + } + + protected function _destroy() + { + if ($this->_update_id === NULL) + { + // Session has not been created yet + return TRUE; + } + + // Delete the current session + $query = DB::delete($this->_table) + ->where($this->_columns['session_id'], '=', ':id') + ->param(':id', $this->_update_id); + + try + { + // Execute the query + $query->execute($this->_db); + + // Delete the cookie + Cookie::delete($this->_name); + } + catch (Exception $e) + { + // An error occurred, the session has not been deleted + return FALSE; + } + + return TRUE; + } + + protected function _gc() + { + if ($this->_lifetime) + { + // Expire sessions when their lifetime is up + $expires = $this->_lifetime; + } + else + { + // Expire sessions after one month + $expires = Date::MONTH; + } + + // Delete all sessions that have expired + DB::delete($this->_table) + ->where($this->_columns['last_active'], '<', ':time') + ->param(':time', time() - $expires) + ->execute($this->_db); + } + +} // End Session_Database diff --git a/includes/kohana/modules/database/classes/session/database.php b/includes/kohana/modules/database/classes/session/database.php new file mode 100644 index 00000000..1e75223e --- /dev/null +++ b/includes/kohana/modules/database/classes/session/database.php @@ -0,0 +1,3 @@ + array + ( + 'type' => 'mysql', + 'connection' => array( + /** + * The following options are available for MySQL: + * + * string hostname server hostname, or socket + * string database database name + * string username database username + * string password database password + * boolean persistent use persistent connections? + * + * Ports and sockets may be appended to the hostname. + */ + 'hostname' => 'localhost', + 'database' => 'kohana', + 'username' => FALSE, + 'password' => FALSE, + 'persistent' => FALSE, + ), + 'table_prefix' => '', + 'charset' => 'utf8', + 'caching' => FALSE, + 'profiling' => TRUE, + ), + 'alternate' => array( + 'type' => 'pdo', + 'connection' => array( + /** + * The following options are available for PDO: + * + * string dsn Data Source Name + * string username database username + * string password database password + * boolean persistent use persistent connections? + */ + 'dsn' => 'mysql:host=localhost;dbname=kohana', + 'username' => 'root', + 'password' => 'r00tdb', + 'persistent' => FALSE, + ), + /** + * The following extra options are available for PDO: + * + * string identifier set the escaping identifier + */ + 'table_prefix' => '', + 'charset' => 'utf8', + 'caching' => FALSE, + 'profiling' => TRUE, + ), +); \ No newline at end of file diff --git a/includes/kohana/modules/database/config/session.php b/includes/kohana/modules/database/config/session.php new file mode 100644 index 00000000..a4229c71 --- /dev/null +++ b/includes/kohana/modules/database/config/session.php @@ -0,0 +1,27 @@ + array( + /** + * Database settings for session storage. + * + * string group configuation group name + * string table session table name + * integer gc number of requests before gc is invoked + * columns array custom column names + */ + 'group' => 'default', + 'table' => 'sessions', + 'gc' => 500, + 'columns' => array( + /** + * session_id: session identifier + * last_active: timestamp of the last activity + * contents: serialized session data + */ + 'session_id' => 'session_id', + 'last_active' => 'last_active', + 'contents' => 'contents' + ), + ), +); diff --git a/includes/kohana/modules/image/README.markdown b/includes/kohana/modules/image/README.markdown new file mode 100644 index 00000000..e69de29b diff --git a/includes/kohana/modules/image/classes/image.php b/includes/kohana/modules/image/classes/image.php new file mode 100644 index 00000000..5b047749 --- /dev/null +++ b/includes/kohana/modules/image/classes/image.php @@ -0,0 +1,3 @@ + Kohana::debug_path($file))); + } + + // Store the image information + $this->file = $file; + $this->width = $info[0]; + $this->height = $info[1]; + $this->type = $info[2]; + $this->mime = image_type_to_mime_type($this->type); + } + + /** + * Render the current image. + * + * echo $image; + * + * [!!] The output of this function is binary and must be rendered with the + * appropriate Content-Type header or it will not be displayed correctly! + * + * @return string + */ + public function __toString() + { + try + { + // Render the current image + return $this->render(); + } + catch (Exception $e) + { + if (is_object(Kohana::$log)) + { + // Get the text of the exception + $error = Kohana::exception_text($e); + + // Add this exception to the log + Kohana::$log->add(Kohana::ERROR, $error); + } + + // Showing any kind of error will be "inside" image data + return ''; + } + } + + /** + * Resize the image to the given size. Either the width or the height can + * be omitted and the image will be resized proportionally. + * + * // Resize to 200 pixels on the shortest side + * $image->resize(200, 200); + * + * // Resize to 200x200 pixels, keeping aspect ratio + * $image->resize(200, 200, Image::INVERSE); + * + * // Resize to 500 pixel width, keeping aspect ratio + * $image->resize(500, NULL); + * + * // Resize to 500 pixel height, keeping aspect ratio + * $image->resize(NULL, 500); + * + * // Resize to 200x500 pixels, ignoring aspect ratio + * $image->resize(200, 500, Image::NONE); + * + * @param integer new width + * @param integer new height + * @param integer master dimension + * @return $this + * @uses Image::_do_resize + */ + public function resize($width = NULL, $height = NULL, $master = NULL) + { + if ($master === NULL) + { + // Choose the master dimension automatically + $master = Image::AUTO; + } + // Image::WIDTH and Image::HEIGHT depricated. You can use it in old projects, + // but in new you must pass empty value for non-master dimension + elseif ($master == Image::WIDTH AND ! empty($width)) + { + $master = Image::AUTO; + + // Set empty height for backvard compatibility + $height = NULL; + } + elseif ($master == Image::HEIGHT AND ! empty($height)) + { + $master = Image::AUTO; + + // Set empty width for backvard compatibility + $width = NULL; + } + + if (empty($width)) + { + if ($master === Image::NONE) + { + // Use the current width + $width = $this->width; + } + else + { + // If width not set, master will be height + $master = Image::HEIGHT; + } + } + + if (empty($height)) + { + if ($master === Image::NONE) + { + // Use the current height + $height = $this->height; + } + else + { + // If height not set, master will be width + $master = Image::WIDTH; + } + } + + switch ($master) + { + case Image::AUTO: + // Choose direction with the greatest reduction ratio + $master = ($this->width / $width) > ($this->height / $height) ? Image::WIDTH : Image::HEIGHT; + break; + case Image::INVERSE: + // Choose direction with the minimum reduction ratio + $master = ($this->width / $width) > ($this->height / $height) ? Image::HEIGHT : Image::WIDTH; + break; + } + + switch ($master) + { + case Image::WIDTH: + // Recalculate the height based on the width proportions + $height = $this->height * $width / $this->width; + break; + case Image::HEIGHT: + // Recalculate the width based on the height proportions + $width = $this->width * $height / $this->height; + break; + } + + // Convert the width and height to integers, minimum value is 1px + $width = max(round($width), 1); + $height = max(round($height), 1); + + $this->_do_resize($width, $height); + + return $this; + } + + /** + * Crop an image to the given size. Either the width or the height can be + * omitted and the current width or height will be used. + * + * If no offset is specified, the center of the axis will be used. + * If an offset of TRUE is specified, the bottom of the axis will be used. + * + * // Crop the image to 200x200 pixels, from the center + * $image->crop(200, 200); + * + * @param integer new width + * @param integer new height + * @param mixed offset from the left + * @param mixed offset from the top + * @return $this + * @uses Image::_do_crop + */ + public function crop($width, $height, $offset_x = NULL, $offset_y = NULL) + { + if ($width > $this->width) + { + // Use the current width + $width = $this->width; + } + + if ($height > $this->height) + { + // Use the current height + $height = $this->height; + } + + if ($offset_x === NULL) + { + // Center the X offset + $offset_x = round(($this->width - $width) / 2); + } + elseif ($offset_x === TRUE) + { + // Bottom the X offset + $offset_x = $this->width - $width; + } + elseif ($offset_x < 0) + { + // Set the X offset from the right + $offset_x = $this->width - $width + $offset_x; + } + + if ($offset_y === NULL) + { + // Center the Y offset + $offset_y = round(($this->height - $height) / 2); + } + elseif ($offset_y === TRUE) + { + // Bottom the Y offset + $offset_y = $this->height - $height; + } + elseif ($offset_y < 0) + { + // Set the Y offset from the bottom + $offset_y = $this->height - $height + $offset_y; + } + + // Determine the maximum possible width and height + $max_width = $this->width - $offset_x; + $max_height = $this->height - $offset_y; + + if ($width > $max_width) + { + // Use the maximum available width + $width = $max_width; + } + + if ($height > $max_height) + { + // Use the maximum available height + $height = $max_height; + } + + $this->_do_crop($width, $height, $offset_x, $offset_y); + + return $this; + } + + /** + * Rotate the image by a given amount. + * + * // Rotate 45 degrees clockwise + * $image->rotate(45); + * + * // Rotate 90% counter-clockwise + * $image->rotate(-90); + * + * @param integer degrees to rotate: -360-360 + * @return $this + * @uses Image::_do_rotate + */ + public function rotate($degrees) + { + // Make the degrees an integer + $degrees = (int) $degrees; + + if ($degrees > 180) + { + do + { + // Keep subtracting full circles until the degrees have normalized + $degrees -= 360; + } + while($degrees > 180); + } + + if ($degrees < -180) + { + do + { + // Keep adding full circles until the degrees have normalized + $degrees += 360; + } + while($degrees < -180); + } + + $this->_do_rotate($degrees); + + return $this; + } + + /** + * Flip the image along the horizontal or vertical axis. + * + * // Flip the image from top to bottom + * $image->flip(Image::HORIZONTAL); + * + * // Flip the image from left to right + * $image->flip(Image::VERTICAL); + * + * @param integer direction: Image::HORIZONTAL, Image::VERTICAL + * @return $this + * @uses Image::_do_flip + */ + public function flip($direction) + { + if ($direction !== Image::HORIZONTAL) + { + // Flip vertically + $direction = Image::VERTICAL; + } + + $this->_do_flip($direction); + + return $this; + } + + /** + * Sharpen the image by a given amount. + * + * // Sharpen the image by 20% + * $image->sharpen(20); + * + * @param integer amount to sharpen: 1-100 + * @return $this + * @uses Image::_do_sharpen + */ + public function sharpen($amount) + { + // The amount must be in the range of 1 to 100 + $amount = min(max($amount, 1), 100); + + $this->_do_sharpen($amount); + + return $this; + } + + /** + * Add a reflection to an image. The most opaque part of the reflection + * will be equal to the opacity setting and fade out to full transparent. + * Alpha transparency is preserved. + * + * // Create a 50 pixel reflection that fades from 0-100% opacity + * $image->reflection(50); + * + * // Create a 50 pixel reflection that fades from 100-0% opacity + * $image->reflection(50, 100, TRUE); + * + * // Create a 50 pixel reflection that fades from 0-60% opacity + * $image->reflection(50, 60, TRUE); + * + * [!!] By default, the reflection will be go from transparent at the top + * to opaque at the bottom. + * + * @param integer reflection height + * @param integer reflection opacity: 0-100 + * @param boolean TRUE to fade in, FALSE to fade out + * @return $this + * @uses Image::_do_reflection + */ + public function reflection($height = NULL, $opacity = 100, $fade_in = FALSE) + { + if ($height === NULL OR $height > $this->height) + { + // Use the current height + $height = $this->height; + } + + // The opacity must be in the range of 0 to 100 + $opacity = min(max($opacity, 0), 100); + + $this->_do_reflection($height, $opacity, $fade_in); + + return $this; + } + + /** + * Add a watermark to an image with a specified opacity. Alpha transparency + * will be preserved. + * + * If no offset is specified, the center of the axis will be used. + * If an offset of TRUE is specified, the bottom of the axis will be used. + * + * // Add a watermark to the bottom right of the image + * $mark = Image::factory('upload/watermark.png'); + * $image->watermark($mark, TRUE, TRUE); + * + * @param object watermark Image instance + * @param integer offset from the left + * @param integer offset from the top + * @param integer opacity of watermark: 1-100 + * @return $this + * @uses Image::_do_watermark + */ + public function watermark(Image $watermark, $offset_x = NULL, $offset_y = NULL, $opacity = 100) + { + if ($offset_x === NULL) + { + // Center the X offset + $offset_x = round(($this->width - $watermark->width) / 2); + } + elseif ($offset_x === TRUE) + { + // Bottom the X offset + $offset_x = $this->width - $watermark->width; + } + elseif ($offset_x < 0) + { + // Set the X offset from the right + $offset_x = $this->width - $watermark->width + $offset_x; + } + + if ($offset_y === NULL) + { + // Center the Y offset + $offset_y = round(($this->height - $watermark->height) / 2); + } + elseif ($offset_y === TRUE) + { + // Bottom the Y offset + $offset_y = $this->height - $watermark->height; + } + elseif ($offset_y < 0) + { + // Set the Y offset from the bottom + $offset_y = $this->height - $watermark->height + $offset_y; + } + + // The opacity must be in the range of 1 to 100 + $opacity = min(max($opacity, 1), 100); + + $this->_do_watermark($watermark, $offset_x, $offset_y, $opacity); + + return $this; + } + + /** + * Set the background color of an image. This is only useful for images + * with alpha transparency. + * + * // Make the image background black + * $image->background('#000'); + * + * // Make the image background black with 50% opacity + * $image->background('#000', 50); + * + * @param string hexadecimal color value + * @param integer background opacity: 0-100 + * @return $this + * @uses Image::_do_background + */ + public function background($color, $opacity = 100) + { + if ($color[0] === '#') + { + // Remove the pound + $color = substr($color, 1); + } + + if (strlen($color) === 3) + { + // Convert shorthand into longhand hex notation + $color = preg_replace('/./', '$0$0', $color); + } + + // Convert the hex into RGB values + list ($r, $g, $b) = array_map('hexdec', str_split($color, 2)); + + // The opacity must be in the range of 0 to 100 + $opacity = min(max($opacity, 0), 100); + + $this->_do_background($r, $g, $b, $opacity); + + return $this; + } + + /** + * Save the image. If the filename is omitted, the original image will + * be overwritten. + * + * // Save the image as a PNG + * $image->save('saved/cool.png'); + * + * // Overwrite the original image + * $image->save(); + * + * [!!] If the file exists, but is not writable, an exception will be thrown. + * + * [!!] If the file does not exist, and the directory is not writable, an + * exception will be thrown. + * + * @param string new image path + * @param integer quality of image: 1-100 + * @return boolean + * @uses Image::_save + * @throws Kohana_Exception + */ + public function save($file = NULL, $quality = 100) + { + if ($file === NULL) + { + // Overwrite the file + $file = $this->file; + } + + if (is_file($file)) + { + if ( ! is_writable($file)) + { + throw new Kohana_Exception('File must be writable: :file', + array(':file' => Kohana::debug_path($file))); + } + } + else + { + // Get the directory of the file + $directory = realpath(pathinfo($file, PATHINFO_DIRNAME)); + + if ( ! is_dir($directory) OR ! is_writable($directory)) + { + throw new Kohana_Exception('Directory must be writable: :directory', + array(':directory' => Kohana::debug_path($directory))); + } + } + + // The quality must be in the range of 1 to 100 + $quality = min(max($quality, 1), 100); + + return $this->_do_save($file, $quality); + } + + /** + * Render the image and return the binary string. + * + * // Render the image at 50% quality + * $data = $image->render(NULL, 50); + * + * // Render the image as a PNG + * $data = $image->render('png'); + * + * @param string image type to return: png, jpg, gif, etc + * @param integer quality of image: 1-100 + * @return string + * @uses Image::_do_render + */ + public function render($type = NULL, $quality = 100) + { + if ($type === NULL) + { + // Use the current image type + $type = image_type_to_extension($this->type, FALSE); + } + + return $this->_do_render($type, $quality); + } + + /** + * Execute a resize. + * + * @param integer new width + * @param integer new height + * @return void + */ + abstract protected function _do_resize($width, $height); + + /** + * Execute a crop. + * + * @param integer new width + * @param integer new height + * @param integer offset from the left + * @param integer offset from the top + * @return void + */ + abstract protected function _do_crop($width, $height, $offset_x, $offset_y); + + /** + * Execute a rotation. + * + * @param integer degrees to rotate + * @return void + */ + abstract protected function _do_rotate($degrees); + + /** + * Execute a flip. + * + * @param integer direction to flip + * @return void + */ + abstract protected function _do_flip($direction); + + /** + * Execute a sharpen. + * + * @param integer amount to sharpen + * @return void + */ + abstract protected function _do_sharpen($amount); + + /** + * Execute a reflection. + * + * @param integer reflection height + * @param integer reflection opacity + * @param boolean TRUE to fade out, FALSE to fade in + * @return void + */ + abstract protected function _do_reflection($height, $opacity, $fade_in); + + /** + * Execute a watermarking. + * + * @param object watermarking Image + * @param integer offset from the left + * @param integer offset from the top + * @param integer opacity of watermark + * @return void + */ + abstract protected function _do_watermark(Image $image, $offset_x, $offset_y, $opacity); + + /** + * Execute a background. + * + * @param integer red + * @param integer green + * @param integer blue + * @param integer opacity + * @return void + */ + abstract protected function _do_background($r, $g, $b, $opacity); + + /** + * Execute a save. + * + * @param string new image filename + * @param integer quality + * @return boolean + */ + abstract protected function _do_save($file, $quality); + + /** + * Execute a render. + * + * @param string image type: png, jpg, gif, etc + * @param integer quality + * @return string + */ + abstract protected function _do_render($type, $quality); + +} // End Image diff --git a/includes/kohana/modules/image/classes/kohana/image/gd.php b/includes/kohana/modules/image/classes/kohana/image/gd.php new file mode 100644 index 00000000..46bd07ba --- /dev/null +++ b/includes/kohana/modules/image/classes/kohana/image/gd.php @@ -0,0 +1,582 @@ +=')) + { + throw new Kohana_Exception('Image_GD requires GD version :required or greater, you have :version', + array('required' => '2.0.1', ':version' => $version)); + } + + return Image_GD::$_checked = TRUE; + } + + // Temporary image resource + protected $_image; + + // Function name to open Image + protected $_create_function; + + /** + * Runs [Image_GD::check] and loads the image. + * + * @return void + * @throws Kohana_Exception + */ + public function __construct($file) + { + if ( ! Image_GD::$_checked) + { + // Run the install check + Image_GD::check(); + } + + parent::__construct($file); + + // Set the image creation function name + switch ($this->type) + { + case IMAGETYPE_JPEG: + $create = 'imagecreatefromjpeg'; + break; + case IMAGETYPE_GIF: + $create = 'imagecreatefromgif'; + break; + case IMAGETYPE_PNG: + $create = 'imagecreatefrompng'; + break; + } + + if ( ! isset($create) OR ! function_exists($create)) + { + throw new Kohana_Exception('Installed GD does not support :type images', + array(':type' => image_type_to_extension($this->type, FALSE))); + } + + // Save function for future use + $this->_create_function = $create; + + // Save filename for lazy loading + $this->_image = $this->file; + } + + /** + * Destroys the loaded image to free up resources. + * + * @return void + */ + public function __destruct() + { + if (is_resource($this->_image)) + { + // Free all resources + imagedestroy($this->_image); + } + } + + /** + * Loads an image into GD. + * + * @return void + */ + protected function _load_image() + { + if ( ! is_resource($this->_image)) + { + // Gets create function + $create = $this->_create_function; + + // Open the temporary image + $this->_image = $create($this->file); + + // Preserve transparency when saving + imagesavealpha($this->_image, TRUE); + } + } + + protected function _do_resize($width, $height) + { + // Presize width and height + $pre_width = $this->width; + $pre_height = $this->height; + + // Loads image if not yet loaded + $this->_load_image(); + + // Test if we can do a resize without resampling to speed up the final resize + if ($width > ($this->width / 2) AND $height > ($this->height / 2)) + { + // The maximum reduction is 10% greater than the final size + $reduction_width = round($width * 1.1); + $reduction_height = round($height * 1.1); + + while ($pre_width / 2 > $reduction_width AND $pre_height / 2 > $reduction_height) + { + // Reduce the size using an O(2n) algorithm, until it reaches the maximum reduction + $pre_width /= 2; + $pre_height /= 2; + } + + // Create the temporary image to copy to + $image = $this->_create($pre_width, $pre_height); + + if (imagecopyresized($image, $this->_image, 0, 0, 0, 0, $pre_width, $pre_height, $this->width, $this->height)) + { + // Swap the new image for the old one + imagedestroy($this->_image); + $this->_image = $image; + } + } + + // Create the temporary image to copy to + $image = $this->_create($width, $height); + + // Execute the resize + if (imagecopyresampled($image, $this->_image, 0, 0, 0, 0, $width, $height, $pre_width, $pre_height)) + { + // Swap the new image for the old one + imagedestroy($this->_image); + $this->_image = $image; + + // Reset the width and height + $this->width = imagesx($image); + $this->height = imagesy($image); + } + } + + protected function _do_crop($width, $height, $offset_x, $offset_y) + { + // Create the temporary image to copy to + $image = $this->_create($width, $height); + + // Loads image if not yet loaded + $this->_load_image(); + + // Execute the crop + if (imagecopyresampled($image, $this->_image, 0, 0, $offset_x, $offset_y, $width, $height, $width, $height)) + { + // Swap the new image for the old one + imagedestroy($this->_image); + $this->_image = $image; + + // Reset the width and height + $this->width = imagesx($image); + $this->height = imagesy($image); + } + } + + protected function _do_rotate($degrees) + { + if ( ! Image_GD::$_bundled) + { + throw new Kohana_Exception('This method requires :function, which is only available in the bundled version of GD', + array(':function' => 'imagerotate')); + } + + // Loads image if not yet loaded + $this->_load_image(); + + // Transparent black will be used as the background for the uncovered region + $transparent = imagecolorallocatealpha($this->_image, 0, 0, 0, 127); + + // Rotate, setting the transparent color + $image = imagerotate($this->_image, 360 - $degrees, $transparent, 1); + + // Save the alpha of the rotated image + imagesavealpha($image, TRUE); + + // Get the width and height of the rotated image + $width = imagesx($image); + $height = imagesy($image); + + if (imagecopymerge($this->_image, $image, 0, 0, 0, 0, $width, $height, 100)) + { + // Swap the new image for the old one + imagedestroy($this->_image); + $this->_image = $image; + + // Reset the width and height + $this->width = $width; + $this->height = $height; + } + } + + protected function _do_flip($direction) + { + // Create the flipped image + $flipped = $this->_create($this->width, $this->height); + + // Loads image if not yet loaded + $this->_load_image(); + + if ($direction === Image::HORIZONTAL) + { + for ($x = 0; $x < $this->width; $x++) + { + // Flip each row from top to bottom + imagecopy($flipped, $this->_image, $x, 0, $this->width - $x - 1, 0, 1, $this->height); + } + } + else + { + for ($y = 0; $y < $this->height; $y++) + { + // Flip each column from left to right + imagecopy($flipped, $this->_image, 0, $y, 0, $this->height - $y - 1, $this->width, 1); + } + } + + // Swap the new image for the old one + imagedestroy($this->_image); + $this->_image = $flipped; + + // Reset the width and height + $this->width = imagesx($flipped); + $this->height = imagesy($flipped); + } + + protected function _do_sharpen($amount) + { + if ( ! Image_GD::$_bundled) + { + throw new Kohana_Exception('This method requires :function, which is only available in the bundled version of GD', + array(':function' => 'imageconvolution')); + } + + // Loads image if not yet loaded + $this->_load_image(); + + // Amount should be in the range of 18-10 + $amount = round(abs(-18 + ($amount * 0.08)), 2); + + // Gaussian blur matrix + $matrix = array + ( + array(-1, -1, -1), + array(-1, $amount, -1), + array(-1, -1, -1), + ); + + // Perform the sharpen + if (imageconvolution($this->_image, $matrix, $amount - 8, 0)) + { + // Reset the width and height + $this->width = imagesx($this->_image); + $this->height = imagesy($this->_image); + } + } + + protected function _do_reflection($height, $opacity, $fade_in) + { + if ( ! Image_GD::$_bundled) + { + throw new Kohana_Exception('This method requires :function, which is only available in the bundled version of GD', + array(':function' => 'imagefilter')); + } + + // Loads image if not yet loaded + $this->_load_image(); + + // Convert an opacity range of 0-100 to 127-0 + $opacity = round(abs(($opacity * 127 / 100) - 127)); + + if ($opacity < 127) + { + // Calculate the opacity stepping + $stepping = (127 - $opacity) / $height; + } + else + { + // Avoid a "divide by zero" error + $stepping = 127 / $height; + } + + // Create the reflection image + $reflection = $this->_create($this->width, $this->height + $height); + + // Copy the image to the reflection + imagecopy($reflection, $this->_image, 0, 0, 0, 0, $this->width, $this->height); + + for ($offset = 0; $height >= $offset; $offset++) + { + // Read the next line down + $src_y = $this->height - $offset - 1; + + // Place the line at the bottom of the reflection + $dst_y = $this->height + $offset; + + if ($fade_in === TRUE) + { + // Start with the most transparent line first + $dst_opacity = round($opacity + ($stepping * ($height - $offset))); + } + else + { + // Start with the most opaque line first + $dst_opacity = round($opacity + ($stepping * $offset)); + } + + // Create a single line of the image + $line = $this->_create($this->width, 1); + + // Copy a single line from the current image into the line + imagecopy($line, $this->_image, 0, 0, 0, $src_y, $this->width, 1); + + // Colorize the line to add the correct alpha level + imagefilter($line, IMG_FILTER_COLORIZE, 0, 0, 0, $dst_opacity); + + // Copy a the line into the reflection + imagecopy($reflection, $line, 0, $dst_y, 0, 0, $this->width, 1); + } + + // Swap the new image for the old one + imagedestroy($this->_image); + $this->_image = $reflection; + + // Reset the width and height + $this->width = imagesx($reflection); + $this->height = imagesy($reflection); + } + + protected function _do_watermark(Image $watermark, $offset_x, $offset_y, $opacity) + { + if ( ! Image_GD::$_bundled) + { + throw new Kohana_Exception('This method requires :function, which is only available in the bundled version of GD', + array(':function' => 'imagelayereffect')); + } + + // Loads image if not yet loaded + $this->_load_image(); + + // Create the watermark image resource + $overlay = imagecreatefromstring($watermark->render()); + + // Get the width and height of the watermark + $width = imagesx($overlay); + $height = imagesy($overlay); + + if ($opacity < 100) + { + // Convert an opacity range of 0-100 to 127-0 + $opacity = round(abs(($opacity * 127 / 100) - 127)); + + // Allocate transparent white + $color = imagecolorallocatealpha($overlay, 255, 255, 255, $opacity); + + // The transparent image will overlay the watermark + imagelayereffect($overlay, IMG_EFFECT_OVERLAY); + + // Fill the background with transparent white + imagefilledrectangle($overlay, 0, 0, $width, $height, $color); + } + + // Alpha blending must be enabled on the background! + imagealphablending($this->_image, TRUE); + + if (imagecopy($this->_image, $overlay, $offset_x, $offset_y, 0, 0, $width, $height)) + { + // Destroy the overlay image + imagedestroy($overlay); + } + } + + protected function _do_background($r, $g, $b, $opacity) + { + // Loads image if not yet loaded + $this->_load_image(); + + // Convert an opacity range of 0-100 to 127-0 + $opacity = round(abs(($opacity * 127 / 100) - 127)); + + // Create a new background + $background = $this->_create($this->width, $this->height); + + // Allocate the color + $color = imagecolorallocatealpha($background, $r, $g, $b, $opacity); + + // Fill the image with white + imagefilledrectangle($background, 0, 0, $this->width, $this->height, $color); + + // Alpha blending must be enabled on the background! + imagealphablending($background, TRUE); + + // Copy the image onto a white background to remove all transparency + if (imagecopy($background, $this->_image, 0, 0, 0, 0, $this->width, $this->height)) + { + // Swap the new image for the old one + imagedestroy($this->_image); + $this->_image = $background; + } + } + + protected function _do_save($file, $quality) + { + // Loads image if not yet loaded + $this->_load_image(); + + // Get the extension of the file + $extension = pathinfo($file, PATHINFO_EXTENSION); + + // Get the save function and IMAGETYPE + list($save, $type) = $this->_save_function($extension, $quality); + + // Save the image to a file + $status = isset($quality) ? $save($this->_image, $file, $quality) : $save($this->_image, $file); + + if ($status === TRUE AND $type !== $this->type) + { + // Reset the image type and mime type + $this->type = $type; + $this->mime = image_type_to_mime_type($type); + } + + return TRUE; + } + + protected function _do_render($type, $quality) + { + // Loads image if not yet loaded + $this->_load_image(); + + // Get the save function and IMAGETYPE + list($save, $type) = $this->_save_function($type, $quality); + + // Capture the output + ob_start(); + + // Render the image + $status = isset($quality) ? $save($this->_image, NULL, $quality) : $save($this->_image, NULL); + + if ($status === TRUE AND $type !== $this->type) + { + // Reset the image type and mime type + $this->type = $type; + $this->mime = image_type_to_mime_type($type); + } + + return ob_get_clean(); + } + + /** + * Get the GD saving function and image type for this extension. + * Also normalizes the quality setting + * + * @param string image type: png, jpg, etc + * @param integer image quality + * @return array save function, IMAGETYPE_* constant + * @throws Kohana_Exception + */ + protected function _save_function($extension, & $quality) + { + switch (strtolower($extension)) + { + case 'jpg': + case 'jpeg': + // Save a JPG file + $save = 'imagejpeg'; + $type = IMAGETYPE_JPEG; + break; + case 'gif': + // Save a GIF file + $save = 'imagegif'; + $type = IMAGETYPE_GIF; + + // GIFs do not a quality setting + $quality = NULL; + break; + case 'png': + // Save a PNG file + $save = 'imagepng'; + $type = IMAGETYPE_PNG; + + // Use a compression level of 9 (does not affect quality!) + $quality = 9; + break; + default: + throw new Kohana_Exception('Installed GD does not support :type images', + array(':type' => $extension)); + break; + } + + return array($save, $type); + } + + /** + * Create an empty image with the given width and height. + * + * @param integer image width + * @param integer image height + * @return resource + */ + protected function _create($width, $height) + { + // Create an empty image + $image = imagecreatetruecolor($width, $height); + + // Do not apply alpha blending + imagealphablending($image, FALSE); + + // Save alpha levels + imagesavealpha($image, TRUE); + + return $image; + } + +} // End Image_GD diff --git a/includes/kohana/modules/oauth/README.md b/includes/kohana/modules/oauth/README.md new file mode 100755 index 00000000..e4e845c7 --- /dev/null +++ b/includes/kohana/modules/oauth/README.md @@ -0,0 +1,5 @@ +# OAuth for Kohana + +An implementation of the [OAuth](http://oauth.net/) protocol. + +*Does not provide server capabilities!* diff --git a/includes/kohana/modules/oauth/classes/kohana/oauth.php b/includes/kohana/modules/oauth/classes/kohana/oauth.php new file mode 100755 index 00000000..505ca95c --- /dev/null +++ b/includes/kohana/modules/oauth/classes/kohana/oauth.php @@ -0,0 +1,217 @@ + $value) + { + if (is_array($value)) + { + // OAuth Spec 9.1.1 (1) + // "If two or more parameters share the same name, they are sorted by their value." + $value = natsort($value); + + foreach ($value as $duplicate) + { + $query[] = $name.'='.$duplicate; + } + } + else + { + $query[] = $name.'='.$value; + } + } + + return implode('&', $query); + } + + /** + * Parse the query string out of the URL and return it as parameters. + * All GET parameters must be removed from the request URL when building + * the base string and added to the request parameters. + * + * // parsed parameters: array('oauth_key' => 'abcdef123456789') + * list($url, $params) = OAuth::parse_url('http://example.com/oauth/access?oauth_key=abcdef123456789'); + * + * [!!] This implements [OAuth Spec 9.1.1](http://oauth.net/core/1.0/#rfc.section.9.1.1). + * + * @param string URL to parse + * @return array (clean_url, params) + * @uses OAuth::parse_params + */ + public static function parse_url($url) + { + if ($query = parse_url($url, PHP_URL_QUERY)) + { + // Remove the query string from the URL + list($url) = explode('?', $url, 2); + + // Parse the query string as request parameters + $params = OAuth::parse_params($query); + } + else + { + // No parameters are present + $params = array(); + } + + return array($url, $params); + } + + /** + * Parse the parameters in a string and return an array. Duplicates are + * converted into indexed arrays. + * + * // Parsed: array('a' => '1', 'b' => '2', 'c' => '3') + * $params = OAuth::parse_params('a=1,b=2,c=3'); + * + * // Parsed: array('a' => array('1', '2'), 'c' => '3') + * $params = OAuth::parse_params('a=1,a=2,c=3'); + * + * @param string parameter string + * @return array + */ + public static function parse_params($params) + { + // Split the parameters by & + $params = explode('&', trim($params)); + + // Create an array of parsed parameters + $parsed = array(); + + foreach ($params as $param) + { + // Split the parameter into name and value + list($name, $value) = explode('=', $param, 2); + + // Decode the name and value + $name = OAuth::urldecode($name); + $value = OAuth::urldecode($value); + + if (isset($parsed[$name])) + { + if ( ! is_array($parsed[$name])) + { + // Convert the parameter to an array + $parsed[$name] = array($parsed[$name]); + } + + // Add a new duplicate parameter + $parsed[$name][] = $value; + } + else + { + // Add a new parameter + $parsed[$name] = $value; + } + } + + return $parsed; + } + +} // End OAuth diff --git a/includes/kohana/modules/oauth/classes/kohana/oauth/consumer.php b/includes/kohana/modules/oauth/classes/kohana/oauth/consumer.php new file mode 100755 index 00000000..f8791da6 --- /dev/null +++ b/includes/kohana/modules/oauth/classes/kohana/oauth/consumer.php @@ -0,0 +1,99 @@ + 'key')); + } + + if ( ! isset($options['secret'])) + { + throw new Kohana_OAuth_Exception('Required option not passed: :option', + array(':option' => 'secret')); + } + + $this->key = $options['key']; + + $this->secret = $options['secret']; + + if (isset($options['callback'])) + { + $this->callback = $options['callback']; + } + } + + /** + * Return the value of any protected class variable. + * + * // Get the consumer key + * $key = $consumer->key; + * + * @param string variable name + * @return mixed + */ + public function __get($key) + { + return $this->$key; + } + + /** + * Change the consumer callback. + * + * @param string new consumer callback + * @return $this + */ + public function callback($callback) + { + $this->callback = $callback; + + return $this; + } + +} // End OAuth_Consumer diff --git a/includes/kohana/modules/oauth/classes/kohana/oauth/exception.php b/includes/kohana/modules/oauth/classes/kohana/oauth/exception.php new file mode 100755 index 00000000..7953dcef --- /dev/null +++ b/includes/kohana/modules/oauth/classes/kohana/oauth/exception.php @@ -0,0 +1,12 @@ +signature = $options['signature']; + } + + if ( ! is_object($this->signature)) + { + // Convert the signature name into an object + $this->signature = OAuth_Signature::factory($this->signature); + } + + if ( ! $this->name) + { + // Attempt to guess the name from the class name + $this->name = strtolower(substr(get_class($this), strlen('OAuth_Provider_'))); + } + } + + /** + * Return the value of any protected class variable. + * + * // Get the provider signature + * $signature = $provider->signature; + * + * @param string variable name + * @return mixed + */ + public function __get($key) + { + return $this->$key; + } + + /** + * Returns the request token URL for the provider. + * + * $url = $provider->url_request_token(); + * + * @return string + */ + abstract public function url_request_token(); + + /** + * Returns the authorization URL for the provider. + * + * $url = $provider->url_authorize(); + * + * @return string + */ + abstract public function url_authorize(); + + /** + * Returns the access token endpoint for the provider. + * + * $url = $provider->url_access_token(); + * + * @return string + */ + abstract public function url_access_token(); + + /** + * Ask for a request token from the OAuth provider. + * + * $token = $provider->request_token($consumer); + * + * @param OAuth_Consumer consumer + * @param array additional request parameters + * @return OAuth_Token_Request + * @uses OAuth_Request_Token + */ + public function request_token(OAuth_Consumer $consumer, array $params = NULL) + { + // Create a new GET request for a request token with the required parameters + $request = OAuth_Request::factory('token', 'GET', $this->url_request_token(), array( + 'oauth_consumer_key' => $consumer->key, + 'oauth_callback' => $consumer->callback, + )); + + if ($params) + { + // Load user parameters + $request->params($params); + } + + // Sign the request using only the consumer, no token is available yet + $request->sign($this->signature, $consumer); + + // Create a response from the request + $response = $request->execute(); + + // Store this token somewhere useful + return OAuth_Token::factory('request', array( + 'token' => $response->param('oauth_token'), + 'secret' => $response->param('oauth_token_secret'), + )); + } + + /** + * Get the authorization URL for the request token. + * + * $this->request->redirect($provider->authorize_url($token)); + * + * @param OAuth_Token_Request token + * @param array additional request parameters + * @return string + */ + public function authorize_url(OAuth_Token_Request $token, array $params = NULL) + { + // Create a new GET request for a request token with the required parameters + $request = OAuth_Request::factory('authorize', 'GET', $this->url_authorize(), array( + 'oauth_token' => $token->token, + )); + + if ($params) + { + // Load user parameters + $request->params($params); + } + + return $request->as_url(); + } + + /** + * Exchange the request token for an access token. + * + * $token = $provider->access_token($consumer, $token); + * + * @param OAuth_Consumer consumer + * @param OAuth_Token_Request token + * @param array additional request parameters + * @return OAuth_Token_Access + */ + public function access_token(OAuth_Consumer $consumer, OAuth_Token_Request $token, array $params = NULL) + { + // Create a new GET request for a request token with the required parameters + $request = OAuth_Request::factory('access', 'GET', $this->url_access_token(), array( + 'oauth_consumer_key' => $consumer->key, + 'oauth_token' => $token->token, + 'oauth_verifier' => $token->verifier, + )); + + if ($params) + { + // Load user parameters + $request->params($params); + } + + // Sign the request using only the consumer, no token is available yet + $request->sign($this->signature, $consumer, $token); + + // Create a response from the request + $response = $request->execute(); + + // Store this token somewhere useful + return OAuth_Token::factory('access', array( + 'token' => $response->param('oauth_token'), + 'secret' => $response->param('oauth_token_secret'), + )); + } + +} // End OAuth_Signature diff --git a/includes/kohana/modules/oauth/classes/kohana/oauth/provider/google.php b/includes/kohana/modules/oauth/classes/kohana/oauth/provider/google.php new file mode 100644 index 00000000..609f2528 --- /dev/null +++ b/includes/kohana/modules/oauth/classes/kohana/oauth/provider/google.php @@ -0,0 +1,55 @@ +. + * Individual Google APIs have separate documentation. A complete list is + * available at . + * + * [!!] This class does not implement any Google API. It is only an + * implementation of standard OAuth with Google as the service provider. + * + * @package Kohana/OAuth + * @category Provider + * @author Kohana Team + * @copyright (c) 2010 Kohana Team + * @license http://kohanaframework.org/license + * @since 3.0.7 + */ +class Kohana_OAuth_Provider_Google extends OAuth_Provider { + + public $name = 'google'; + + protected $signature = 'HMAC-SHA1'; + + public function url_request_token() + { + return 'https://www.google.com/accounts/OAuthGetRequestToken'; + } + + public function url_authorize() + { + return 'https://www.google.com/accounts/OAuthAuthorizeToken'; + } + + public function url_access_token() + { + return 'https://www.google.com/accounts/OAuthGetAccessToken'; + } + + public function request_token(OAuth_Consumer $consumer, array $params = NULL) + { + if ( ! isset($params['scope'])) + { + // All request tokens must specify the data scope to access + // http://code.google.com/apis/accounts/docs/OAuth.html#prepScope + throw new Kohana_OAuth_Exception('Required parameter to not passed: :param', array( + ':param' => 'scope', + )); + } + + return parent::request_token($consumer, $params); + } + +} // End OAuth_Provider_Google diff --git a/includes/kohana/modules/oauth/classes/kohana/oauth/provider/twitter.php b/includes/kohana/modules/oauth/classes/kohana/oauth/provider/twitter.php new file mode 100644 index 00000000..46fa52de --- /dev/null +++ b/includes/kohana/modules/oauth/classes/kohana/oauth/provider/twitter.php @@ -0,0 +1,39 @@ +. + * + * [!!] This class does not implement the Twitter API. It is only an + * implementation of standard OAuth with Twitter as the service provider. + * + * @package Kohana/OAuth + * @category Provider + * @author Kohana Team + * @copyright (c) 2010 Kohana Team + * @license http://kohanaframework.org/license + * @since 3.0.7 + */ +class Kohana_OAuth_Provider_Twitter extends OAuth_Provider { + + public $name = 'twitter'; + + protected $signature = 'HMAC-SHA1'; + + public function url_request_token() + { + return 'https://api.twitter.com/oauth/request_token'; + } + + public function url_authorize() + { + return 'https://api.twitter.com/oauth/authenticate'; + } + + public function url_access_token() + { + return 'https://api.twitter.com/oauth/access_token'; + } + +} // End OAuth_Provider_Twitter diff --git a/includes/kohana/modules/oauth/classes/kohana/oauth/request.php b/includes/kohana/modules/oauth/classes/kohana/oauth/request.php new file mode 100755 index 00000000..bfaaf5cf --- /dev/null +++ b/includes/kohana/modules/oauth/classes/kohana/oauth/request.php @@ -0,0 +1,478 @@ +method = strtoupper($method); + } + + // Separate the URL and query string, which will be used as additional + // default parameters + list ($url, $default) = OAuth::parse_url($url); + + // Set the request URL + $this->url = $url; + + if ($default) + { + // Set the default parameters + $this->params($default); + } + + if ($params) + { + // Set the request parameters + $this->params($params); + } + + if ($this->required('oauth_version') AND ! isset($this->params['oauth_version'])) + { + // Set the version of this request + $this->params['oauth_version'] = OAuth::$version; + } + + if ($this->required('oauth_timestamp') AND ! isset($this->params['oauth_timestamp'])) + { + // Set the timestamp of this request + $this->params['oauth_timestamp'] = $this->timestamp(); + } + + if ($this->required('oauth_nonce') AND ! isset($this->params['oauth_nonce'])) + { + // Set the unique nonce of this request + $this->params['oauth_nonce'] = $this->nonce(); + } + } + + /** + * Return the value of any protected class variable. + * + * // Get the request parameters + * $params = $request->params; + * + * // Get the request URL + * $url = $request->url; + * + * @param string variable name + * @return mixed + */ + public function __get($key) + { + return $this->$key; + } + + /** + * Generates the UNIX timestamp for a request. + * + * $time = $request->timestamp(); + * + * [!!] This method implements [OAuth 1.0 Spec 8](http://oauth.net/core/1.0/#rfc.section.8). + * + * @return integer + */ + public function timestamp() + { + return time(); + } + + /** + * Generates the nonce for a request. + * + * $nonce = $request->nonce(); + * + * [!!] This method implements [OAuth 1.0 Spec 8](http://oauth.net/core/1.0/#rfc.section.8). + * + * @return string + * @uses Text::random + */ + public function nonce() + { + return Text::random('alnum', 40); + } + + /** + * Get the base signature string for a request. + * + * $base = $request->base_string(); + * + * [!!] This method implements [OAuth 1.0 Spec A5.1](http://oauth.net/core/1.0/#rfc.section.A.5.1). + * + * @param OAuth_Request request to sign + * @return string + * @uses OAuth::urlencode + * @uses OAuth::normalize_params + */ + public function base_string() + { + $url = $this->url; + + // Get the request parameters + $params = $this->params; + + // "oauth_signature" is never included in the base string! + unset($params['oauth_signature']); + + // method & url & sorted-parameters + return implode('&', array( + $this->method, + OAuth::urlencode($url), + OAuth::urlencode(OAuth::normalize_params($params)), + )); + } + + /** + * Parameter getter and setter. Setting the value to `NULL` will remove it. + * + * // Set the "oauth_consumer_key" to a new value + * $request->param('oauth_consumer_key', $key); + * + * // Get the "oauth_consumer_key" value + * $key = $request->param('oauth_consumer_key'); + * + * // Remove "oauth_consumer_key" + * $request->param('oauth_consumer_key', NULL); + * + * @param string parameter name + * @param mixed parameter value + * @param boolean allow duplicates? + * @return mixed when getting + * @return $this when setting + * @uses Arr::get + */ + public function param($name, $value = NULL, $duplicate = FALSE) + { + if (func_num_args() < 2) + { + // Get the parameter + return Arr::get($this->params, $name); + } + + if ($value === NULL) + { + // Remove the parameter + unset($this->params[$name]); + } + else + { + if (isset($this->params[$name]) AND $duplicate) + { + if ( ! is_array($this->params[$name])) + { + // Convert the parameter into an array + $this->params[$name] = array($this->params[$name]); + } + + // Add the duplicate value + $this->params[$name][] = $value; + } + else + { + // Set the parameter value + $this->params[$name] = $value; + } + } + + return $this; + } + + /** + * Set multiple parameters. + * + * $request->params($params); + * + * @param array parameters + * @param boolean allow duplicates? + * @return $this + * @uses OAuth_Request::param + */ + public function params(array $params, $duplicate = FALSE) + { + foreach ($params as $name => $value) + { + $this->param($name, $value, $duplicate); + } + + return $this; + } + + /** + * Get and set required parameters. + * + * $request->required($field, $value); + * + * @param string parameter name + * @param boolean field value + * @return boolean when getting + * @return $this when setting + */ + public function required($param, $value = NULL) + { + if ($value === NULL) + { + return ! empty($this->required[$param]); + } + + // Change the requirement value + $this->required[$param] = (boolean) $value; + + return $this; + } + + /** + * Convert the request parameters into an `Authorization` header. + * + * $header = $request->as_header(); + * + * [!!] This method implements [OAuth 1.0 Spec 5.4.1](http://oauth.net/core/1.0/#rfc.section.5.4.1). + * + * @return string + */ + public function as_header() + { + $header = array(); + + foreach ($this->params as $name => $value) + { + if (strpos($name, 'oauth_') === 0) + { + // OAuth Spec 5.4.1 + // "Parameter names and values are encoded per Parameter Encoding [RFC 3986]." + $header[] = OAuth::urlencode($name).'="'.OAuth::urlencode($value).'"'; + } + } + + return 'OAuth '.implode(', ', $header); + } + + /** + * Convert the request parameters into a query string, suitable for GET and + * POST requests. + * + * $query = $request->as_query(); + * + * [!!] This method implements [OAuth 1.0 Spec 5.2 (2,3)](http://oauth.net/core/1.0/#rfc.section.5.2). + * + * @param boolean include oauth parameters + * @return string + */ + public function as_query($include_oauth = NULL) + { + if ($include_oauth !== TRUE AND $this->send_header) + { + // If we are sending a header, OAuth parameters should not be + // included in the query string. + + $params = array(); + foreach ($this->params as $name => $value) + { + if (strpos($name, 'oauth_') !== 0) + { + // This is not an OAuth parameter + $params[$name] = $value; + } + } + } + else + { + $params = $this->params; + } + + return OAuth::normalize_params($params); + } + + /** + * Return the entire request URL with the parameters as a GET string. + * + * $url = $request->as_url(); + * + * @return string + * @uses OAuth_Request::as_query + */ + public function as_url() + { + return $this->url.'?'.$this->as_query(TRUE); + } + + /** + * Sign the request, setting the `oauth_signature_method` and `oauth_signature`. + * + * @param OAuth_Signature signature + * @param OAuth_Consumer consumer + * @param OAuth_Token token + * @return $this + * @uses OAuth_Signature::sign + */ + public function sign(OAuth_Signature $signature, OAuth_Consumer $consumer, OAuth_Token $token = NULL) + { + // Create a new signature class from the method + $this->param('oauth_signature_method', $signature->name); + + // Sign the request using the consumer and token + $this->param('oauth_signature', $signature->sign($this, $consumer, $token)); + + return $this; + } + + /** + * Checks that all required request parameters have been set. Throws an + * exception if any parameters are missing. + * + * try + * { + * $request->check(); + * } + * catch (OAuth_Exception $e) + * { + * // Request has missing parameters + * } + * + * @return TRUE + * @throws Kohana_OAuth_Exception + */ + public function check() + { + foreach ($this->required as $param => $required) + { + if ($required AND ! isset($this->params[$param])) + { + throw new Kohana_OAuth_Exception('Request to :url requires missing parameter ":param"', array( + ':url' => $this->url, + ':param' => $param, + )); + } + } + + return TRUE; + } + + /** + * Execute the request and return a response. + * + * @param string request type: GET, POST, etc (NULL for header) + * @param array additional cURL options + * @return string request response body + * @uses OAuth_Request::check + * @uses Arr::get + * @uses Remote::get + */ + public function execute(array $options = NULL) + { + // Check that all required fields are set + $this->check(); + + // Get the URL of the request + $url = $this->url; + + if ( ! isset($options[CURLOPT_CONNECTTIMEOUT])) + { + // Use the request default timeout + $options[CURLOPT_CONNECTTIMEOUT] = $this->timeout; + } + + if ($this->send_header) + { + // Get the the current headers + $headers = Arr::get($options, CURLOPT_HTTPHEADER, array()); + + // Add the Authorization header + $headers[] = 'Authorization: '.$this->as_header(); + + // Store the new headers + $options[CURLOPT_HTTPHEADER] = $headers; + } + + if ($this->method === 'POST') + { + // Send the request as a POST + $options[CURLOPT_POST] = TRUE; + + if ($post = $this->as_query()) + { + // Attach the post fields to the request + $options[CURLOPT_POSTFIELDS] = $post; + } + } + elseif ($query = $this->as_query()) + { + // Append the parameters to the query string + $url = "{$url}?{$query}"; + } + + return Remote::get($url, $options); + } + +} // End OAuth_Request diff --git a/includes/kohana/modules/oauth/classes/kohana/oauth/request/access.php b/includes/kohana/modules/oauth/classes/kohana/oauth/request/access.php new file mode 100755 index 00000000..629abf5a --- /dev/null +++ b/includes/kohana/modules/oauth/classes/kohana/oauth/request/access.php @@ -0,0 +1,32 @@ + TRUE, + 'oauth_token' => TRUE, + 'oauth_signature_method' => TRUE, + 'oauth_signature' => TRUE, + 'oauth_timestamp' => TRUE, + 'oauth_nonce' => TRUE, + 'oauth_verifier' => TRUE, + 'oauth_version' => TRUE, + ); + + public function execute(array $options = NULL) + { + return OAuth_Response::factory(parent::execute($options)); + } + +} // End OAuth_Request_Access diff --git a/includes/kohana/modules/oauth/classes/kohana/oauth/request/authorize.php b/includes/kohana/modules/oauth/classes/kohana/oauth/request/authorize.php new file mode 100755 index 00000000..e31dc9b5 --- /dev/null +++ b/includes/kohana/modules/oauth/classes/kohana/oauth/request/authorize.php @@ -0,0 +1,26 @@ + TRUE, + ); + + public function execute(array $options = NULL) + { + return Request::instance()->redirect($this->as_url()); + } + +} // End OAuth_Request_Authorize diff --git a/includes/kohana/modules/oauth/classes/kohana/oauth/request/credentials.php b/includes/kohana/modules/oauth/classes/kohana/oauth/request/credentials.php new file mode 100755 index 00000000..ea65e31d --- /dev/null +++ b/includes/kohana/modules/oauth/classes/kohana/oauth/request/credentials.php @@ -0,0 +1,16 @@ + TRUE, + 'oauth_token' => TRUE, + 'oauth_signature_method' => TRUE, + 'oauth_signature' => TRUE, + 'oauth_timestamp' => TRUE, + 'oauth_nonce' => TRUE, + 'oauth_version' => TRUE, + ); + +} // End OAuth_Request_Resource diff --git a/includes/kohana/modules/oauth/classes/kohana/oauth/request/token.php b/includes/kohana/modules/oauth/classes/kohana/oauth/request/token.php new file mode 100644 index 00000000..267a3cca --- /dev/null +++ b/includes/kohana/modules/oauth/classes/kohana/oauth/request/token.php @@ -0,0 +1,32 @@ + TRUE, + 'oauth_consumer_key' => TRUE, + 'oauth_signature_method' => TRUE, + 'oauth_signature' => TRUE, + 'oauth_timestamp' => TRUE, + 'oauth_nonce' => TRUE, + 'oauth_version' => TRUE, + ); + + public function execute(array $options = NULL) + { + return OAuth_Response::factory(parent::execute($options)); + } + +} // End OAuth_Request_Token diff --git a/includes/kohana/modules/oauth/classes/kohana/oauth/response.php b/includes/kohana/modules/oauth/classes/kohana/oauth/response.php new file mode 100755 index 00000000..1fa1db74 --- /dev/null +++ b/includes/kohana/modules/oauth/classes/kohana/oauth/response.php @@ -0,0 +1,51 @@ +params = OAuth::parse_params($body); + } + } + + /** + * Return the value of any protected class variable. + * + * // Get the response parameters + * $params = $response->params; + * + * @param string variable name + * @return mixed + */ + public function __get($key) + { + return $this->$key; + } + + public function param($name, $default = NULL) + { + return Arr::get($this->params, $name, $default); + } + +} // End OAuth_Response diff --git a/includes/kohana/modules/oauth/classes/kohana/oauth/server.php b/includes/kohana/modules/oauth/classes/kohana/oauth/server.php new file mode 100755 index 00000000..e5ca414e --- /dev/null +++ b/includes/kohana/modules/oauth/classes/kohana/oauth/server.php @@ -0,0 +1,14 @@ +name; + * + * @param string variable name + * @return mixed + */ + public function __get($key) + { + return $this->$key; + } + + /** + * Get a signing key from a consumer and token. + * + * $key = $signature->key($consumer, $token); + * + * [!!] This method implements the signing key of [OAuth 1.0 Spec 9](http://oauth.net/core/1.0/#rfc.section.9). + * + * @param OAuth_Consumer consumer + * @param OAuth_Token token + * @return string + * @uses OAuth::urlencode + */ + public function key(OAuth_Consumer $consumer, OAuth_Token $token = NULL) + { + $key = OAuth::urlencode($consumer->secret).'&'; + + if ($token) + { + $key .= OAuth::urlencode($token->secret); + } + + return $key; + } + + abstract public function sign(OAuth_Request $request, OAuth_Consumer $consumer, OAuth_Token $token = NULL); + + abstract public function verify($signature, OAuth_Request $request, OAuth_Consumer $consumer, OAuth_Token $token = NULL); + +} // End OAuth_Signature diff --git a/includes/kohana/modules/oauth/classes/kohana/oauth/signature/hmac/sha1.php b/includes/kohana/modules/oauth/classes/kohana/oauth/signature/hmac/sha1.php new file mode 100644 index 00000000..d77ee20c --- /dev/null +++ b/includes/kohana/modules/oauth/classes/kohana/oauth/signature/hmac/sha1.php @@ -0,0 +1,68 @@ +sign($request, $consumer, $token); + * + * [!!] This method implements [OAuth 1.0 Spec 9.2.1](http://oauth.net/core/1.0/#rfc.section.9.2.1). + * + * @param OAuth_Request request + * @param OAuth_Consumer consumer + * @param OAuth_Token token + * @return string + * @uses OAuth_Signature::key + * @uses OAuth_Request::base_string + */ + public function sign(OAuth_Request $request, OAuth_Consumer $consumer, OAuth_Token $token = NULL) + { + // Get the signing key + $key = $this->key($consumer, $token); + + // Get the base string for the signature + $base_string = $request->base_string(); + + // Sign the base string using the key + return base64_encode(hash_hmac('sha1', $base_string, $key, TRUE)); + } + + /** + * Verify a HMAC-SHA1 signature. + * + * if ( ! $signature->verify($signature, $request, $consumer, $token)) + * { + * throw new Kohana_OAuth_Exception('Failed to verify signature'); + * } + * + * [!!] This method implements [OAuth 1.0 Spec 9.2.2](http://oauth.net/core/1.0/#rfc.section.9.2.2). + * + * @param string signature to verify + * @param OAuth_Request request + * @param OAuth_Consumer consumer + * @param OAuth_Token token + * @return boolean + * @uses OAuth_Signature_HMAC_SHA1::sign + */ + public function verify($signature, OAuth_Request $request, OAuth_Consumer $consumer, OAuth_Token $token = NULL) + { + return $signature === $this->sign($request, $consumer, $token); + } + +} // End OAuth_Signature_HMAC_SHA1 diff --git a/includes/kohana/modules/oauth/classes/kohana/oauth/signature/plaintext.php b/includes/kohana/modules/oauth/classes/kohana/oauth/signature/plaintext.php new file mode 100644 index 00000000..db5a1125 --- /dev/null +++ b/includes/kohana/modules/oauth/classes/kohana/oauth/signature/plaintext.php @@ -0,0 +1,57 @@ +sign($request, $consumer, $token); + * + * [!!] This method implements [OAuth 1.0 Spec 9.4.1](http://oauth.net/core/1.0/#rfc.section.9.4.1). + * + * @param OAuth_Request request + * @param OAuth_Consumer consumer + * @param OAuth_Token token + * @return $this + */ + public function sign(OAuth_Request $request, OAuth_Consumer $consumer, OAuth_Token $token = NULL) + { + // Use the signing key as the signature + return $this->key($consumer, $token); + } + + /** + * Verify a plaintext signature. + * + * if ( ! $signature->verify($signature, $request, $consumer, $token)) + * { + * throw new Kohana_OAuth_Exception('Failed to verify signature'); + * } + * + * [!!] This method implements [OAuth 1.0 Spec 9.4.2](http://oauth.net/core/1.0/#rfc.section.9.4.2). + * + * @param string signature to verify + * @param OAuth_Request request + * @param OAuth_Consumer consumer + * @param OAuth_Token token + * @return boolean + * @uses OAuth_Signature_PLAINTEXT::sign + */ + public function verify($signature, OAuth_Request $request, OAuth_Consumer $consumer, OAuth_Token $token = NULL) + { + return $signature === $this->key($consumer, $token); + } + +} // End OAuth_Signature_PLAINTEXT diff --git a/includes/kohana/modules/oauth/classes/kohana/oauth/token.php b/includes/kohana/modules/oauth/classes/kohana/oauth/token.php new file mode 100755 index 00000000..b474098a --- /dev/null +++ b/includes/kohana/modules/oauth/classes/kohana/oauth/token.php @@ -0,0 +1,94 @@ + 'token')); + } + + if ( ! isset($options['secret'])) + { + throw new Kohana_OAuth_Exception('Required option not passed: :option', + array(':option' => 'secret')); + } + + $this->token = $options['token']; + + $this->secret = $options['secret']; + } + + /** + * Return the value of any protected class variable. + * + * // Get the token secret + * $secret = $token->secret; + * + * @param string variable name + * @return mixed + */ + public function __get($key) + { + return $this->$key; + } + + /** + * Returns the token key. + * + * @return string + */ + public function __toString() + { + return (string) $this->token; + } + +} // End OAuth_Token diff --git a/includes/kohana/modules/oauth/classes/kohana/oauth/token/access.php b/includes/kohana/modules/oauth/classes/kohana/oauth/token/access.php new file mode 100644 index 00000000..7d0564d0 --- /dev/null +++ b/includes/kohana/modules/oauth/classes/kohana/oauth/token/access.php @@ -0,0 +1,16 @@ +verifier($key); + * + * @param string new verifier + * @return $this + */ + public function verifier($verifier) + { + $this->verifier = $verifier; + + return $this; + } + +} // End OAuth_Token_Request diff --git a/includes/kohana/modules/oauth/classes/oauth.php b/includes/kohana/modules/oauth/classes/oauth.php new file mode 100644 index 00000000..f73d3480 --- /dev/null +++ b/includes/kohana/modules/oauth/classes/oauth.php @@ -0,0 +1,3 @@ + array( + 'key' => 'your consumer key', + 'secret' => 'your consumer secret' + ), +); \ No newline at end of file diff --git a/includes/kohana/modules/oauth/config/userguide.php b/includes/kohana/modules/oauth/config/userguide.php new file mode 100644 index 00000000..828837ff --- /dev/null +++ b/includes/kohana/modules/oauth/config/userguide.php @@ -0,0 +1,23 @@ + array( + + // This should be the path to this modules userguide pages, without the 'guide/'. Ex: '/guide/modulename/' would be 'modulename' + 'oauth' => array( + + // Whether this modules userguide pages should be shown + 'enabled' => TRUE, + + // The name that should show up on the userguide index page + 'name' => 'OAuth', + + // A short description of this module, shown on the index page + 'description' => 'Official OAuth module, used for open protocol authorization.', + + // Copyright message, shown in the footer for this module + 'copyright' => '© 2008–2010 Kohana Team', + ) + ) +); \ No newline at end of file diff --git a/includes/kohana/modules/oauth/guide/oauth/config.md b/includes/kohana/modules/oauth/guide/oauth/config.md new file mode 100644 index 00000000..3bd58f55 --- /dev/null +++ b/includes/kohana/modules/oauth/guide/oauth/config.md @@ -0,0 +1,24 @@ +# OAuth Configuration + +All configuration for OAuth is done in the `config/oauth.php` file. The configuration file is organized by the provider name. + +## Example Configuration File + + return array( + /** + * Twitter application registration: https://twitter.com/apps + */ + 'twitter' => array( + 'key' => 'your consumer key', + 'secret' => 'your consumer secret' + ), + /** + * Google application registration: https://www.google.com/accounts/ManageDomains + */ + 'google' => array( + 'key' => 'your domain name', + 'secret' => 'your consumer secret' + ), + ); + +[!!] The consumer key and secret **must** be defined for all providers. diff --git a/includes/kohana/modules/oauth/guide/oauth/index.md b/includes/kohana/modules/oauth/guide/oauth/index.md new file mode 100644 index 00000000..d69b93b6 --- /dev/null +++ b/includes/kohana/modules/oauth/guide/oauth/index.md @@ -0,0 +1,14 @@ +# About OAuth + +[OAuth](http://oauth.net/) is an open protocol to allow secure API authorization in a simple and standard method from desktop and web applications. This module provides a pure PHP implementation of the OAuth v1.0 protocol, with support for PLAINTEXT and HMAC-SHA1 signatures. + +## Supported Providers + +The following providers are available by default: + +* [Twitter](http://twitter.com/) using [OAuth_Provider_Twitter] +* [Google](http://www.google.com/) using [OAuth_Provider_Google] + +Additional providers can be created by creating an extension of [OAuth_Provider]. + + diff --git a/includes/kohana/modules/oauth/guide/oauth/menu.md b/includes/kohana/modules/oauth/guide/oauth/menu.md new file mode 100644 index 00000000..853d7cf3 --- /dev/null +++ b/includes/kohana/modules/oauth/guide/oauth/menu.md @@ -0,0 +1,3 @@ +## [OAuth]() +- [Configuration](config) +- [Usage](usage) diff --git a/includes/kohana/modules/oauth/guide/oauth/usage.md b/includes/kohana/modules/oauth/guide/oauth/usage.md new file mode 100644 index 00000000..9ee458d8 --- /dev/null +++ b/includes/kohana/modules/oauth/guide/oauth/usage.md @@ -0,0 +1,3 @@ +# OAuth Usage + +[!!] This is a stub. Please see [controller/oauth source](http://github.com/kohana/oauth/tree/206c20033e209f233c9e7dc72836bfb786bd7e34/classes/controller) for now. The methods [OAuth_Provider_Google::user_profile] and [OAuth_Provider_Twitter::update_status] no longer exist in the current git master branch. diff --git a/includes/kohana/modules/oauth/views/oauth/twitter/tweet.php b/includes/kohana/modules/oauth/views/oauth/twitter/tweet.php new file mode 100644 index 00000000..9811a9a7 --- /dev/null +++ b/includes/kohana/modules/oauth/views/oauth/twitter/tweet.php @@ -0,0 +1,15 @@ + + +
+
+
+
+ + + + + + +

Response from Twitter:

+ + diff --git a/includes/kohana/modules/orm/classes/kohana/orm.php b/includes/kohana/modules/orm/classes/kohana/orm.php new file mode 100644 index 00000000..d5ee716c --- /dev/null +++ b/includes/kohana/modules/orm/classes/kohana/orm.php @@ -0,0 +1,1346 @@ +_object_name = strtolower(substr(get_class($this), 6)); + $this->_object_plural = Inflector::plural($this->_object_name); + + if ( ! isset($this->_sorting)) + { + // Default sorting + $this->_sorting = array($this->_primary_key => 'ASC'); + } + + if ( ! empty($this->_ignored_columns)) + { + // Optimize for performance + $this->_ignored_columns = array_combine($this->_ignored_columns, $this->_ignored_columns); + } + + // Initialize database + $this->_initialize(); + + // Clear the object + $this->clear(); + + if ($id !== NULL) + { + if (is_array($id)) + { + foreach ($id as $column => $value) + { + // Passing an array of column => values + $this->where($column, '=', $value); + } + + $this->find(); + } + else + { + // Passing the primary key + + // Set the object's primary key, but don't load it until needed + $this->_object[$this->_primary_key] = $id; + + // Object is considered saved until something is set + $this->_saved = TRUE; + } + } + elseif ( ! empty($this->_preload_data)) + { + // Load preloaded data from a database call cast + $this->_load_values($this->_preload_data); + + $this->_preload_data = array(); + } + } + + /** + * Checks if object data is set. + * + * @param string column name + * @return boolean + */ + public function __isset($column) + { + $this->_load(); + + return + ( + isset($this->_object[$column]) OR + isset($this->_related[$column]) OR + isset($this->_has_one[$column]) OR + isset($this->_belongs_to[$column]) OR + isset($this->_has_many[$column]) + ); + } + + /** + * Unsets object data. + * + * @param string column name + * @return void + */ + public function __unset($column) + { + $this->_load(); + + unset($this->_object[$column], $this->_changed[$column], $this->_related[$column]); + } + + /** + * Displays the primary key of a model when it is converted to a string. + * + * @return string + */ + public function __toString() + { + return (string) $this->pk(); + } + + /** + * Allows serialization of only the object data and state, to prevent + * "stale" objects being unserialized, which also requires less memory. + * + * @return array + */ + public function __sleep() + { + // Store only information about the object + return array('_object_name', '_object', '_changed', '_loaded', '_saved', '_sorting'); + } + + /** + * Prepares the database connection and reloads the object. + * + * @return void + */ + public function __wakeup() + { + // Initialize database + $this->_initialize(); + + if ($this->_reload_on_wakeup === TRUE) + { + // Reload the object + $this->reload(); + } + } + + /** + * Handles pass-through to database methods. Calls to query methods + * (query, get, insert, update) are not allowed. Query builder methods + * are chainable. + * + * @param string method name + * @param array method arguments + * @return mixed + */ + public function __call($method, array $args) + { + if (in_array($method, ORM::$_properties)) + { + if ($method === 'loaded') + { + if ( ! isset($this->_object_name)) + { + // Calling loaded method prior to the object being fully initialized + return FALSE; + } + + $this->_load(); + } + elseif ($method === 'validate') + { + if ( ! isset($this->_validate)) + { + // Initialize the validation object + $this->_validate(); + } + } + + // Return the property + return $this->{'_'.$method}; + } + elseif (in_array($method, ORM::$_db_methods)) + { + // Add pending database call which is executed after query type is determined + $this->_db_pending[] = array('name' => $method, 'args' => $args); + + return $this; + } + else + { + throw new Kohana_Exception('Invalid method :method called in :class', + array(':method' => $method, ':class' => get_class($this))); + } + } + + /** + * Handles retrieval of all model values, relationships, and metadata. + * + * @param string column name + * @return mixed + */ + public function __get($column) + { + if (array_key_exists($column, $this->_object)) + { + $this->_load(); + + return $this->_object[$column]; + } + elseif (isset($this->_related[$column]) AND $this->_related[$column]->_loaded) + { + // Return related model that has already been loaded + return $this->_related[$column]; + } + elseif (isset($this->_belongs_to[$column])) + { + $this->_load(); + + $model = $this->_related($column); + + // Use this model's column and foreign model's primary key + $col = $model->_table_name.'.'.$model->_primary_key; + $val = $this->_object[$this->_belongs_to[$column]['foreign_key']]; + + $model->where($col, '=', $val)->find(); + + return $this->_related[$column] = $model; + } + elseif (isset($this->_has_one[$column])) + { + $model = $this->_related($column); + + // Use this model's primary key value and foreign model's column + $col = $model->_table_name.'.'.$this->_has_one[$column]['foreign_key']; + $val = $this->pk(); + + $model->where($col, '=', $val)->find(); + + return $this->_related[$column] = $model; + } + elseif (isset($this->_has_many[$column])) + { + $model = ORM::factory($this->_has_many[$column]['model']); + + if (isset($this->_has_many[$column]['through'])) + { + // Grab has_many "through" relationship table + $through = $this->_has_many[$column]['through']; + + // Join on through model's target foreign key (far_key) and target model's primary key + $join_col1 = $through.'.'.$this->_has_many[$column]['far_key']; + $join_col2 = $model->_table_name.'.'.$model->_primary_key; + + $model->join($through)->on($join_col1, '=', $join_col2); + + // Through table's source foreign key (foreign_key) should be this model's primary key + $col = $through.'.'.$this->_has_many[$column]['foreign_key']; + $val = $this->pk(); + } + else + { + // Simple has_many relationship, search where target model's foreign key is this model's primary key + $col = $model->_table_name.'.'.$this->_has_many[$column]['foreign_key']; + $val = $this->pk(); + } + + return $model->where($col, '=', $val); + } + else + { + throw new Kohana_Exception('The :property property does not exist in the :class class', + array(':property' => $column, ':class' => get_class($this))); + } + } + + /** + * Handles setting of all model values, and tracks changes between values. + * + * @param string column name + * @param mixed column value + * @return void + */ + public function __set($column, $value) + { + if ( ! isset($this->_object_name)) + { + // Object not yet constructed, so we're loading data from a database call cast + $this->_preload_data[$column] = $value; + + return; + } + + if (array_key_exists($column, $this->_ignored_columns)) + { + // No processing for ignored columns, just store it + $this->_object[$column] = $value; + } + elseif (array_key_exists($column, $this->_object)) + { + $this->_object[$column] = $value; + + if (isset($this->_table_columns[$column])) + { + // Data has changed + $this->_changed[$column] = $column; + + // Object is no longer saved + $this->_saved = FALSE; + } + } + elseif (isset($this->_belongs_to[$column])) + { + // Update related object itself + $this->_related[$column] = $value; + + // Update the foreign key of this model + $this->_object[$this->_belongs_to[$column]['foreign_key']] = $value->pk(); + + $this->_changed[$column] = $this->_belongs_to[$column]['foreign_key']; + } + else + { + throw new Kohana_Exception('The :property: property does not exist in the :class: class', + array(':property:' => $column, ':class:' => get_class($this))); + } + } + + /** + * Set values from an array with support for one-one relationships. This method should be used + * for loading in post data, etc. + * + * @param array array of key => val + * @return ORM + */ + public function values($values) + { + foreach ($values as $key => $value) + { + if (array_key_exists($key, $this->_object) OR array_key_exists($key, $this->_ignored_columns)) + { + // Property of this model + $this->__set($key, $value); + } + elseif (isset($this->_belongs_to[$key]) OR isset($this->_has_one[$key])) + { + // Value is an array of properties for the related model + $this->_related[$key] = $value; + } + } + + return $this; + } + + /** + * Prepares the model database connection, determines the table name, + * and loads column information. + * + * @return void + */ + protected function _initialize() + { + if ( ! is_object($this->_db)) + { + // Get database instance + $this->_db = Database::instance($this->_db); + } + + if (empty($this->_table_name)) + { + // Table name is the same as the object name + $this->_table_name = $this->_object_name; + + if ($this->_table_names_plural === TRUE) + { + // Make the table name plural + $this->_table_name = Inflector::plural($this->_table_name); + } + } + + foreach ($this->_belongs_to as $alias => $details) + { + $defaults['model'] = $alias; + $defaults['foreign_key'] = $alias.$this->_foreign_key_suffix; + + $this->_belongs_to[$alias] = array_merge($defaults, $details); + } + + foreach ($this->_has_one as $alias => $details) + { + $defaults['model'] = $alias; + $defaults['foreign_key'] = $this->_object_name.$this->_foreign_key_suffix; + + $this->_has_one[$alias] = array_merge($defaults, $details); + } + + foreach ($this->_has_many as $alias => $details) + { + $defaults['model'] = Inflector::singular($alias); + $defaults['foreign_key'] = $this->_object_name.$this->_foreign_key_suffix; + $defaults['through'] = NULL; + $defaults['far_key'] = Inflector::singular($alias).$this->_foreign_key_suffix; + + $this->_has_many[$alias] = array_merge($defaults, $details); + } + + // Load column information + $this->reload_columns(); + } + + /** + * Initializes validation rules, callbacks, filters, and labels + * + * @return void + */ + protected function _validate() + { + $this->_validate = Validate::factory($this->_object); + + foreach ($this->_rules as $field => $rules) + { + $this->_validate->rules($field, $rules); + } + + foreach ($this->_filters as $field => $filters) + { + $this->_validate->filters($field, $filters); + } + + // Use column names by default for labels + $columns = array_keys($this->_table_columns); + + // Merge user-defined labels + $labels = array_merge(array_combine($columns, $columns), $this->_labels); + + foreach ($labels as $field => $label) + { + $this->_validate->label($field, $label); + } + + foreach ($this->_callbacks as $field => $callbacks) + { + foreach ($callbacks as $callback) + { + if (is_string($callback) AND method_exists($this, $callback)) + { + // Callback method exists in current ORM model + $this->_validate->callback($field, array($this, $callback)); + } + else + { + // Try global function + $this->_validate->callback($field, $callback); + } + } + } + } + + /** + * Returns the values of this object as an array, including any related one-one + * models that have already been loaded using with() + * + * @return array + */ + public function as_array() + { + $object = array(); + + foreach ($this->_object as $key => $val) + { + // Call __get for any user processing + $object[$key] = $this->__get($key); + } + + foreach ($this->_related as $key => $model) + { + // Include any related objects that are already loaded + $object[$key] = $model->as_array(); + } + + return $object; + } + + /** + * Binds another one-to-one object to this model. One-to-one objects + * can be nested using 'object1:object2' syntax + * + * @param string target model to bind to + * @return void + */ + public function with($target_path) + { + if (isset($this->_with_applied[$target_path])) + { + // Don't join anything already joined + return $this; + } + + // Split object parts + $aliases = explode(':', $target_path); + $target = $this; + foreach ($aliases as $alias) + { + // Go down the line of objects to find the given target + $parent = $target; + $target = $parent->_related($alias); + + if ( ! $target) + { + // Can't find related object + return $this; + } + } + + // Target alias is at the end + $target_alias = $alias; + + // Pop-off top alias to get the parent path (user:photo:tag becomes user:photo - the parent table prefix) + array_pop($aliases); + $parent_path = implode(':', $aliases); + + if (empty($parent_path)) + { + // Use this table name itself for the parent path + $parent_path = $this->_table_name; + } + else + { + if( ! isset($this->_with_applied[$parent_path])) + { + // If the parent path hasn't been joined yet, do it first (otherwise LEFT JOINs fail) + $this->with($parent_path); + } + } + + // Add to with_applied to prevent duplicate joins + $this->_with_applied[$target_path] = TRUE; + + // Use the keys of the empty object to determine the columns + foreach (array_keys($target->_object) as $column) + { + // Skip over ignored columns + if( ! in_array($column, $target->_ignored_columns)) + { + $name = $target_path.'.'.$column; + $alias = $target_path.':'.$column; + + // Add the prefix so that load_result can determine the relationship + $this->select(array($name, $alias)); + } + } + + if (isset($parent->_belongs_to[$target_alias])) + { + // Parent belongs_to target, use target's primary key and parent's foreign key + $join_col1 = $target_path.'.'.$target->_primary_key; + $join_col2 = $parent_path.'.'.$parent->_belongs_to[$target_alias]['foreign_key']; + } + else + { + // Parent has_one target, use parent's primary key as target's foreign key + $join_col1 = $parent_path.'.'.$parent->_primary_key; + $join_col2 = $target_path.'.'.$parent->_has_one[$target_alias]['foreign_key']; + } + + // Join the related object into the result + $this->join(array($target->_table_name, $target_path), 'LEFT')->on($join_col1, '=', $join_col2); + + return $this; + } + + /** + * Initializes the Database Builder to given query type + * + * @param int Type of Database query + * @return ORM + */ + protected function _build($type) + { + // Construct new builder object based on query type + switch ($type) + { + case Database::SELECT: + $this->_db_builder = DB::select(); + break; + case Database::UPDATE: + $this->_db_builder = DB::update($this->_table_name); + break; + case Database::DELETE: + $this->_db_builder = DB::delete($this->_table_name); + } + + // Process pending database method calls + foreach ($this->_db_pending as $method) + { + $name = $method['name']; + $args = $method['args']; + + $this->_db_applied[$name] = $name; + + call_user_func_array(array($this->_db_builder, $name), $args); + } + + return $this; + } + + /** + * Loads the given model + * + * @return ORM + */ + protected function _load() + { + if ( ! $this->_loaded AND ! $this->empty_pk() AND ! isset($this->_changed[$this->_primary_key])) + { + // Only load if it hasn't been loaded, and a primary key is specified and hasn't been modified + return $this->find($this->pk()); + } + } + + /** + * Finds and loads a single database row into the object. + * + * @chainable + * @param mixed primary key + * @return ORM + */ + public function find($id = NULL) + { + if ( ! empty($this->_load_with)) + { + foreach ($this->_load_with as $alias) + { + // Bind relationship + $this->with($alias); + } + } + + $this->_build(Database::SELECT); + + if ($id !== NULL) + { + // Search for a specific column + $this->_db_builder->where($this->_table_name.'.'.$this->_primary_key, '=', $id); + } + + return $this->_load_result(FALSE); + } + + /** + * Finds multiple database rows and returns an iterator of the rows found. + * + * @chainable + * @return Database_Result + */ + public function find_all() + { + if ( ! empty($this->_load_with)) + { + foreach ($this->_load_with as $alias) + { + // Bind relationship + $this->with($alias); + } + } + + $this->_build(Database::SELECT); + + return $this->_load_result(TRUE); + } + + /** + * Validates the current model's data + * + * @return boolean + */ + public function check() + { + if ( ! isset($this->_validate)) + { + // Initialize the validation object + $this->_validate(); + } + else + { + // Validation object has been created, just exchange the data array + $this->_validate->exchangeArray($this->_object); + } + + if ($this->_validate->check()) + { + // Fields may have been modified by filters + $this->_object = array_merge($this->_object, $this->_validate->getArrayCopy()); + + return TRUE; + } + else + { + return FALSE; + } + } + + /** + * Saves the current object. + * + * @chainable + * @return ORM + */ + public function save() + { + if (empty($this->_changed)) + return $this; + + $data = array(); + foreach ($this->_changed as $column) + { + // Compile changed data + $data[$column] = $this->_object[$column]; + } + + if ( ! $this->empty_pk() AND ! isset($this->_changed[$this->_primary_key])) + { + // Primary key isn't empty and hasn't been changed so do an update + + if (is_array($this->_updated_column)) + { + // Fill the updated column + $column = $this->_updated_column['column']; + $format = $this->_updated_column['format']; + + $data[$column] = $this->_object[$column] = ($format === TRUE) ? time() : date($format); + } + + $query = DB::update($this->_table_name) + ->set($data) + ->where($this->_primary_key, '=', $this->pk()) + ->execute($this->_db); + + // Object has been saved + $this->_saved = TRUE; + } + else + { + if (is_array($this->_created_column)) + { + // Fill the created column + $column = $this->_created_column['column']; + $format = $this->_created_column['format']; + + $data[$column] = $this->_object[$column] = ($format === TRUE) ? time() : date($format); + } + + $result = DB::insert($this->_table_name) + ->columns(array_keys($data)) + ->values(array_values($data)) + ->execute($this->_db); + + if ($result) + { + if ($this->empty_pk()) + { + // Load the insert id as the primary key + // $result is array(insert_id, total_rows) + $this->_object[$this->_primary_key] = $result[0]; + } + + // Object is now loaded and saved + $this->_loaded = $this->_saved = TRUE; + } + } + + if ($this->_saved === TRUE) + { + // All changes have been saved + $this->_changed = array(); + } + + return $this; + } + + /** + * Updates all existing records + * + * @chainable + * @return ORM + */ + public function save_all() + { + $this->_build(Database::UPDATE); + + if (empty($this->_changed)) + return $this; + + $data = array(); + foreach ($this->_changed as $column) + { + // Compile changed data omitting ignored columns + $data[$column] = $this->_object[$column]; + } + + if (is_array($this->_updated_column)) + { + // Fill the updated column + $column = $this->_updated_column['column']; + $format = $this->_updated_column['format']; + + $data[$column] = $this->_object[$column] = ($format === TRUE) ? time() : date($format); + } + + $this->_db_builder->set($data)->execute($this->_db); + + return $this; + } + + /** + * Deletes the current object from the database. This does NOT destroy + * relationships that have been created with other objects. + * + * @chainable + * @param mixed id to delete + * @return ORM + */ + public function delete($id = NULL) + { + if ($id === NULL) + { + // Use the the primary key value + $id = $this->pk(); + } + + if ( ! empty($id) OR $id === '0') + { + // Delete the object + DB::delete($this->_table_name) + ->where($this->_primary_key, '=', $id) + ->execute($this->_db); + } + + return $this; + } + + /** + * Delete all objects in the associated table. This does NOT destroy + * relationships that have been created with other objects. + * + * @chainable + * @return ORM + */ + public function delete_all() + { + $this->_build(Database::DELETE); + + $this->_db_builder->execute($this->_db); + + return $this->clear(); + } + + /** + * Unloads the current object and clears the status. + * + * @chainable + * @return ORM + */ + public function clear() + { + // Create an array with all the columns set to NULL + $values = array_combine(array_keys($this->_table_columns), array_fill(0, count($this->_table_columns), NULL)); + + // Replace the object and reset the object status + $this->_object = $this->_changed = $this->_related = array(); + + // Replace the current object with an empty one + $this->_load_values($values); + + $this->reset(); + + return $this; + } + + /** + * Reloads the current object from the database. + * + * @chainable + * @return ORM + */ + public function reload() + { + $primary_key = $this->pk(); + + // Replace the object and reset the object status + $this->_object = $this->_changed = $this->_related = array(); + + return $this->find($primary_key); + } + + /** + * Reload column definitions. + * + * @chainable + * @param boolean force reloading + * @return ORM + */ + public function reload_columns($force = FALSE) + { + if ($force === TRUE OR empty($this->_table_columns)) + { + if (isset(ORM::$_column_cache[$this->_object_name])) + { + // Use cached column information + $this->_table_columns = ORM::$_column_cache[$this->_object_name]; + } + else + { + // Grab column information from database + $this->_table_columns = $this->list_columns(TRUE); + + // Load column cache + ORM::$_column_cache[$this->_object_name] = $this->_table_columns; + } + } + + return $this; + } + + /** + * Tests if this object has a relationship to a different model. + * + * @param string alias of the has_many "through" relationship + * @param ORM related ORM model + * @return boolean + */ + public function has($alias, $model) + { + // Return count of matches as boolean + return (bool) DB::select(array('COUNT("*")', 'records_found')) + ->from($this->_has_many[$alias]['through']) + ->where($this->_has_many[$alias]['foreign_key'], '=', $this->pk()) + ->where($this->_has_many[$alias]['far_key'], '=', $model->pk()) + ->execute($this->_db) + ->get('records_found'); + } + + /** + * Adds a new relationship to between this model and another. + * + * @param string alias of the has_many "through" relationship + * @param ORM related ORM model + * @param array additional data to store in "through"/pivot table + * @return ORM + */ + public function add($alias, ORM $model, $data = NULL) + { + $columns = array($this->_has_many[$alias]['foreign_key'], $this->_has_many[$alias]['far_key']); + $values = array($this->pk(), $model->pk()); + + if ($data !== NULL) + { + // Additional data stored in pivot table + $columns = array_merge($columns, array_keys($data)); + $values = array_merge($values, array_values($data)); + } + + DB::insert($this->_has_many[$alias]['through']) + ->columns($columns) + ->values($values) + ->execute($this->_db); + + return $this; + } + + /** + * Removes a relationship between this model and another. + * + * @param string alias of the has_many "through" relationship + * @param ORM related ORM model + * @return ORM + */ + public function remove($alias, ORM $model) + { + DB::delete($this->_has_many[$alias]['through']) + ->where($this->_has_many[$alias]['foreign_key'], '=', $this->pk()) + ->where($this->_has_many[$alias]['far_key'], '=', $model->pk()) + ->execute($this->_db); + + return $this; + } + + /** + * Count the number of records in the table. + * + * @return integer + */ + public function count_all() + { + $selects = array(); + + foreach ($this->_db_pending as $key => $method) + { + if ($method['name'] == 'select') + { + // Ignore any selected columns for now + $selects[] = $method; + unset($this->_db_pending[$key]); + } + } + + $this->_build(Database::SELECT); + + $records = (int) $this->_db_builder->from($this->_table_name) + ->select(array('COUNT("*")', 'records_found')) + ->execute($this->_db) + ->get('records_found'); + + // Add back in selected columns + $this->_db_pending += $selects; + + $this->reset(); + + // Return the total number of records in a table + return $records; + } + + /** + * Proxy method to Database list_columns. + * + * @return array + */ + public function list_columns() + { + // Proxy to database + return $this->_db->list_columns($this->_table_name); + } + + /** + * Proxy method to Database field_data. + * + * @chainable + * @param string SQL query to clear + * @return ORM + */ + public function clear_cache($sql = NULL) + { + // Proxy to database + $this->_db->clear_cache($sql); + + ORM::$_column_cache = array(); + + return $this; + } + + /** + * Returns an ORM model for the given one-one related alias + * + * @param string alias name + * @return ORM + */ + protected function _related($alias) + { + if (isset($this->_related[$alias])) + { + return $this->_related[$alias]; + } + elseif (isset($this->_has_one[$alias])) + { + return $this->_related[$alias] = ORM::factory($this->_has_one[$alias]['model']); + } + elseif (isset($this->_belongs_to[$alias])) + { + return $this->_related[$alias] = ORM::factory($this->_belongs_to[$alias]['model']); + } + else + { + return FALSE; + } + } + + /** + * Loads an array of values into into the current object. + * + * @chainable + * @param array values to load + * @return ORM + */ + protected function _load_values(array $values) + { + if (array_key_exists($this->_primary_key, $values)) + { + // Set the loaded and saved object status based on the primary key + $this->_loaded = $this->_saved = ($values[$this->_primary_key] !== NULL); + } + + // Related objects + $related = array(); + + foreach ($values as $column => $value) + { + if (strpos($column, ':') === FALSE) + { + if ( ! isset($this->_changed[$column])) + { + $this->_object[$column] = $value; + } + } + else + { + list ($prefix, $column) = explode(':', $column, 2); + + $related[$prefix][$column] = $value; + } + } + + if ( ! empty($related)) + { + foreach ($related as $object => $values) + { + // Load the related objects with the values in the result + $this->_related($object)->_load_values($values); + } + } + + return $this; + } + + /** + * Loads a database result, either as a new object for this model, or as + * an iterator for multiple rows. + * + * @chainable + * @param boolean return an iterator or load a single row + * @return ORM for single rows + * @return ORM_Iterator for multiple rows + */ + protected function _load_result($multiple = FALSE) + { + $this->_db_builder->from($this->_table_name); + + if ($multiple === FALSE) + { + // Only fetch 1 record + $this->_db_builder->limit(1); + } + + // Select all columns by default + $this->_db_builder->select($this->_table_name.'.*'); + + if ( ! isset($this->_db_applied['order_by']) AND ! empty($this->_sorting)) + { + foreach ($this->_sorting as $column => $direction) + { + if (strpos($column, '.') === FALSE) + { + // Sorting column for use in JOINs + $column = $this->_table_name.'.'.$column; + } + + $this->_db_builder->order_by($column, $direction); + } + } + + if ($multiple === TRUE) + { + // Return database iterator casting to this object type + $result = $this->_db_builder->as_object(get_class($this))->execute($this->_db); + + $this->reset(); + + return $result; + } + else + { + // Load the result as an associative array + $result = $this->_db_builder->as_assoc()->execute($this->_db); + + $this->reset(); + + if ($result->count() === 1) + { + // Load object values + $this->_load_values($result->current()); + } + else + { + // Clear the object, nothing was found + $this->clear(); + } + + return $this; + } + } + + /** + * Returns the value of the primary key + * + * @return mixed primary key + */ + public function pk() + { + return $this->_object[$this->_primary_key]; + } + + /** + * Returns whether or not primary key is empty + * + * @return bool + */ + protected function empty_pk() + { + return (empty($this->_object[$this->_primary_key]) AND $this->_object[$this->_primary_key] !== '0'); + } + + /** + * Returns last executed query + * + * @return string + */ + public function last_query() + { + return $this->_db->last_query; + } + + /** + * Clears query builder. Passing FALSE is useful to keep the existing + * query conditions for another query. + * + * @param bool Pass FALSE to avoid resetting on the next call + */ + public function reset($next = TRUE) + { + if ($next AND $this->_db_reset) + { + $this->_db_pending = array(); + $this->_db_applied = array(); + $this->_db_builder = NULL; + $this->_with_applied = array(); + } + + // Reset on the next call? + $this->_db_reset = $next; + + return $this; + } + +} // End ORM diff --git a/includes/kohana/modules/orm/classes/orm.php b/includes/kohana/modules/orm/classes/orm.php new file mode 100644 index 00000000..63baa131 --- /dev/null +++ b/includes/kohana/modules/orm/classes/orm.php @@ -0,0 +1,3 @@ + array('source' => 'query_string', 'key' => 'page'), + 'total_items' => 0, + 'items_per_page' => 10, + 'view' => 'pagination/basic', + 'auto_hide' => TRUE, + 'first_page_in_url' => FALSE, + ); + + // Current page number + protected $current_page; + + // Total item count + protected $total_items; + + // How many items to show per page + protected $items_per_page; + + // Total page count + protected $total_pages; + + // Item offset for the first item displayed on the current page + protected $current_first_item; + + // Item offset for the last item displayed on the current page + protected $current_last_item; + + // Previous page number; FALSE if the current page is the first one + protected $previous_page; + + // Next page number; FALSE if the current page is the last one + protected $next_page; + + // First page number; FALSE if the current page is the first one + protected $first_page; + + // Last page number; FALSE if the current page is the last one + protected $last_page; + + // Query offset + protected $offset; + + /** + * Creates a new Pagination object. + * + * @param array configuration + * @return Pagination + */ + public static function factory(array $config = array()) + { + return new Pagination($config); + } + + /** + * Creates a new Pagination object. + * + * @param array configuration + * @return void + */ + public function __construct(array $config = array()) + { + // Overwrite system defaults with application defaults + $this->config = $this->config_group() + $this->config; + + // Pagination setup + $this->setup($config); + } + + /** + * Retrieves a pagination config group from the config file. One config group can + * refer to another as its parent, which will be recursively loaded. + * + * @param string pagination config group; "default" if none given + * @return array config settings + */ + public function config_group($group = 'default') + { + // Load the pagination config file + $config_file = Kohana::config('pagination'); + + // Initialize the $config array + $config['group'] = (string) $group; + + // Recursively load requested config groups + while (isset($config['group']) AND isset($config_file->$config['group'])) + { + // Temporarily store config group name + $group = $config['group']; + unset($config['group']); + + // Add config group values, not overwriting existing keys + $config += $config_file->$group; + } + + // Get rid of possible stray config group names + unset($config['group']); + + // Return the merged config group settings + return $config; + } + + /** + * Loads configuration settings into the object and (re)calculates pagination if needed. + * Allows you to update config settings after a Pagination object has been constructed. + * + * @param array configuration + * @return object Pagination + */ + public function setup(array $config = array()) + { + if (isset($config['group'])) + { + // Recursively load requested config groups + $config += $this->config_group($config['group']); + } + + // Overwrite the current config settings + $this->config = $config + $this->config; + + // Only (re)calculate pagination when needed + if ($this->current_page === NULL + OR isset($config['current_page']) + OR isset($config['total_items']) + OR isset($config['items_per_page'])) + { + // Retrieve the current page number + if ( ! empty($this->config['current_page']['page'])) + { + // The current page number has been set manually + $this->current_page = (int) $this->config['current_page']['page']; + } + else + { + switch ($this->config['current_page']['source']) + { + case 'query_string': + $this->current_page = isset($_GET[$this->config['current_page']['key']]) + ? (int) $_GET[$this->config['current_page']['key']] + : 1; + break; + + case 'route': + $this->current_page = (int) Request::current()->param($this->config['current_page']['key'], 1); + break; + } + } + + // Calculate and clean all pagination variables + $this->total_items = (int) max(0, $this->config['total_items']); + $this->items_per_page = (int) max(1, $this->config['items_per_page']); + $this->total_pages = (int) ceil($this->total_items / $this->items_per_page); + $this->current_page = (int) min(max(1, $this->current_page), max(1, $this->total_pages)); + $this->current_first_item = (int) min((($this->current_page - 1) * $this->items_per_page) + 1, $this->total_items); + $this->current_last_item = (int) min($this->current_first_item + $this->items_per_page - 1, $this->total_items); + $this->previous_page = ($this->current_page > 1) ? $this->current_page - 1 : FALSE; + $this->next_page = ($this->current_page < $this->total_pages) ? $this->current_page + 1 : FALSE; + $this->first_page = ($this->current_page === 1) ? FALSE : 1; + $this->last_page = ($this->current_page >= $this->total_pages) ? FALSE : $this->total_pages; + $this->offset = (int) (($this->current_page - 1) * $this->items_per_page); + } + + // Chainable method + return $this; + } + + /** + * Generates the full URL for a certain page. + * + * @param integer page number + * @return string page URL + */ + public function url($page = 1) + { + // Clean the page number + $page = max(1, (int) $page); + + // No page number in URLs to first page + if ($page === 1 AND ! $this->config['first_page_in_url']) + { + $page = NULL; + } + + switch ($this->config['current_page']['source']) + { + case 'query_string': + return URL::site(Request::current()->uri).URL::query(array($this->config['current_page']['key'] => $page)); + + case 'route': + return URL::site(Request::current()->uri(array($this->config['current_page']['key'] => $page))).URL::query(); + } + + return '#'; + } + + /** + * Checks whether the given page number exists. + * + * @param integer page number + * @return boolean + * @since 3.0.7 + */ + public function valid_page($page) + { + // Page number has to be a clean integer + if ( ! Validate::digit($page)) + return FALSE; + + return $page > 0 AND $page <= $this->total_pages; + } + + /** + * Renders the pagination links. + * + * @param mixed string of the view to use, or a Kohana_View object + * @return string pagination output (HTML) + */ + public function render($view = NULL) + { + // Automatically hide pagination whenever it is superfluous + if ($this->config['auto_hide'] === TRUE AND $this->total_pages <= 1) + return ''; + + if ($view === NULL) + { + // Use the view from config + $view = $this->config['view']; + } + + if ( ! $view instanceof Kohana_View) + { + // Load the view file + $view = View::factory($view); + } + + // Pass on the whole Pagination object + return $view->set(get_object_vars($this))->set('page', $this)->render(); + } + + /** + * Renders the pagination links. + * + * @return string pagination output (HTML) + */ + public function __toString() + { + return $this->render(); + } + + /** + * Returns a Pagination property. + * + * @param string URI of the request + * @return mixed Pagination property; NULL if not found + */ + public function __get($key) + { + return isset($this->$key) ? $this->$key : NULL; + } + + /** + * Updates a single config setting, and recalculates pagination if needed. + * + * @param string config key + * @param mixed config value + * @return void + */ + public function __set($key, $value) + { + $this->setup(array($key => $value)); + } + +} // End Pagination \ No newline at end of file diff --git a/includes/kohana/modules/pagination/classes/pagination.php b/includes/kohana/modules/pagination/classes/pagination.php new file mode 100644 index 00000000..6be4b159 --- /dev/null +++ b/includes/kohana/modules/pagination/classes/pagination.php @@ -0,0 +1,3 @@ + array( + 'current_page' => array('source' => 'query_string', 'key' => 'page'), // source: "query_string" or "route" + 'total_items' => 0, + 'items_per_page' => 10, + 'view' => 'pagination/basic', + 'auto_hide' => TRUE, + 'first_page_in_url' => FALSE, + ), + +); diff --git a/includes/kohana/modules/pagination/views/pagination/basic.php b/includes/kohana/modules/pagination/views/pagination/basic.php new file mode 100644 index 00000000..8ec73843 --- /dev/null +++ b/includes/kohana/modules/pagination/views/pagination/basic.php @@ -0,0 +1,37 @@ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

\ No newline at end of file diff --git a/includes/kohana/modules/pagination/views/pagination/floating.php b/includes/kohana/modules/pagination/views/pagination/floating.php new file mode 100644 index 00000000..f776fd14 --- /dev/null +++ b/includes/kohana/modules/pagination/views/pagination/floating.php @@ -0,0 +1,94 @@ += $n4); + +// Point $n3 between $n2 and $n4 +$n3 = (int) (($n2 + $n4) / 2); +$use_n3 = ($use_middle && (($n4 - $n2) > 1)); + +// Point $n6 between $n5 and $n7 +$n6 = (int) (($n5 + $n7) / 2); +$use_n6 = ($use_middle && (($n7 - $n5) > 1)); + +// Links to display as array(page => content) +$links = array(); + +// Generate links data in accordance with calculated numbers +for ($i = $n1; $i <= $n2; $i++) +{ + $links[$i] = $i; +} +if ($use_n3) +{ + $links[$n3] = '…'; +} +for ($i = $n4; $i <= $n5; $i++) +{ + $links[$i] = $i; +} +if ($use_n6) +{ + $links[$n6] = '…'; +} +for ($i = $n7; $i <= $n8; $i++) +{ + $links[$i] = $i; +} + +?> +

+ + + + + + + + + + + + + + $content): ?> + + + + + + + + + + + + + + + + + + + + + +

\ No newline at end of file diff --git a/includes/kohana/modules/unittest/README.markdown b/includes/kohana/modules/unittest/README.markdown new file mode 100644 index 00000000..f19404a3 --- /dev/null +++ b/includes/kohana/modules/unittest/README.markdown @@ -0,0 +1,70 @@ +# Kohana-PHPUnit integration + +This module integrates PHPUnit with Kohana. + +If you look through any of the tests provided in this module you'll probably notice all theHorribleCamelCase. +I've chosen to do this because it's part of the PHPUnit coding conventions and is required for certain features such as auto documentation. + +## Requirements + +* [PHPUnit](http://www.phpunit.de/) >= 3.4 + +### Optional extras + +* The [Archive module](http://github.com/BRMatt/kohana-archive) is required if you want to download code coverage reports from the web ui, however you can also view them without downloading. + +## Installation + +**Step 0**: Download this module! + +To get it from git execute the following command in the root of your project: + + $ git submodule add git://github.com/kohana/unittest.git modules/unittest + +And watch the gitorious magic... + +Of course, you can always download the code from the [github project](http://github.com/kohana/unittest) as an archive. + +The following instructions will assume you've moved it to `modules/unittest`, if you haven't then you should update all paths accordingly. + +**Step 1**: Enable this module in your bootstrap file: + + /** + * Enable modules. Modules are referenced by a relative or absolute path. + */ + Kohana::modules(array( + 'unittest' => MODPATH.'unittest', // PHPUnit integration + )); + +**Step 2**: In your app's bootstrap file modify the lines where the request is handled, which by default looks like: + + /** + * Execute the main request using PATH_INFO. If no URI source is specified, + * the URI will be automatically detected. + */ + echo Request::instance($_SERVER['PATH_INFO']) + ->execute() + ->send_headers() + ->response; + +To: + + if ( ! defined('SUPPRESS_REQUEST')) + { + /** + * Execute the main request using PATH_INFO. If no URI source is specified, + * the URI will be automatically detected. + */ + echo Request::instance($_SERVER['PATH_INFO']) + ->execute() + ->send_headers() + ->response; + } + +**Step 3**: Create a folder called `unittest` in your app's cache dir (`APPPATH/cache`). If you don't want to use this path for storing generated reports, skip this step and change the config file. + +Note: make sure the settings in `config/unittest.php` are correct for your environment. If they aren't, copy the file to `application/config/unittest.php` and change the values accordingly. + +**Step 4**: Start testing! + +You can find more info and tutorials in the [guide/](http://github.com/kohana/unittest/tree/master/guide/) directory. diff --git a/includes/kohana/modules/unittest/classes/controller/unittest.php b/includes/kohana/modules/unittest/classes/controller/unittest.php new file mode 100644 index 00000000..283be07b --- /dev/null +++ b/includes/kohana/modules/unittest/classes/controller/unittest.php @@ -0,0 +1,359 @@ + + * @author Paul Banks + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ + +Class Controller_UnitTest extends Controller_Template +{ + /** + * Whether the archive module is available + * @var boolean + */ + protected $cc_archive_available = FALSE; + + /** + * Unittest config + * @var Kohana_Config + */ + protected $config = NULL; + + /** + * The uri by which the report uri will be executed + * @var string + */ + protected $report_uri = ''; + + /** + * The uri by which the run action will be executed + * @var string + */ + protected $run_uri = ''; + + /** + * Is the XDEBUG extension loaded? + * @var boolean + */ + protected $xdebug_loaded = FALSE; + + /** + * Template + * @var string + */ + public $template = 'unittest/layout'; + + /** + * Loads test suite + */ + public function before() + { + parent::before(); + + if ( ! Kohana_Tests::enabled()) + { + // Pretend this is a normal 404 error... + $this->status = 404; + + throw new Kohana_Request_Exception('Unable to find a route to match the URI: :uri', + array(':uri' => $this->request->uri)); + } + + // Prevent the whitelist from being autoloaded, but allow the blacklist + // to be loaded + Kohana_Tests::configure_environment(FALSE); + + $this->config = Kohana::config('unittest'); + + // This just stops some very very long lines + $route = Route::get('unittest'); + $this->report_uri = $route->uri(array('action' => 'report')); + $this->run_uri = $route->uri(array('action' => 'run')); + + // Switch used to disable cc settings + $this->xdebug_loaded = extension_loaded('xdebug'); + $this->cc_archive_enabled = class_exists('Archive'); + + Kohana_View::set_global('xdebug_enabled', $this->xdebug_loaded); + Kohana_View::set_global('cc_archive_enabled', $this->cc_archive_enabled); + } + + /** + * Handles index page for /unittest/ and /unittest/index/ + */ + public function action_index() + { + $this->template->body = View::factory('unittest/index') + ->set('run_uri', $this->run_uri) + ->set('report_uri', $this->report_uri) + ->set('whitelistable_items', $this->get_whitelistable_items()) + ->set('groups', $this->get_groups_list(Kohana_Tests::suite())); + } + + /** + * Handles report generation + */ + public function action_report() + { + // Fairly foolproof + if ( ! $this->config->cc_report_path AND ! class_exists('Archive')) + { + throw new Kohana_Exception('Cannot generate report'); + } + + // We don't want to use the HTML layout, we're sending the user 100111011100110010101100 + $this->auto_render = FALSE; + + $suite = Kohana_Tests::suite(); + $temp_path = rtrim($this->config->temp_path, '/').'/'; + $group = (array) Arr::get($_GET, 'group', array()); + + // Stop unittest from interpretting "all groups" as "no groups" + if (empty($group) OR empty($group[0])) + { + $group = array(); + } + + if (Arr::get($_GET, 'use_whitelist', FALSE)) + { + $this->whitelist(Arr::get($_GET, 'whitelist', array())); + } + + $runner = new Kohana_Unittest_Runner($suite); + + // If the user wants to download a report + if ($this->cc_archive_enabled AND Arr::get($_GET, 'archive') === '1') + { + // $report is the actual directory of the report, + // $folder is the name component of directory + list($report, $folder) = $runner->generate_report($group, $temp_path); + + $archive = Archive::factory('zip'); + + // TODO: Include the test results? + $archive->add($report, 'report', TRUE); + + $filename = $folder.'.zip'; + + $archive->save($temp_path.$filename); + + // It'd be nice to clear up afterwards but by deleting the report dir we corrupt the archive + // And once the archive has been sent to the user Request stops the script so we can't delete anything + // It'll be up to the user to delete files periodically + $this->request->send_file($temp_path.$filename, $filename); + } + else + { + $folder = trim($this->config->cc_report_path, '/').'/'; + $path = DOCROOT.$folder; + + if ( ! file_exists($path)) + { + throw new Kohana_Exception('Report directory :dir does not exist', array(':dir' => $path)); + } + + if ( ! is_writable($path)) + { + throw new Kohana_Exception('Script doesn\'t have permission to write to report dir :dir ', array(':dir' => $path)); + } + + $runner->generate_report($group, $path, FALSE); + + $this->request->redirect(URL::base(FALSE, TRUE).$folder.'index.html'); + } + } + + /** + * Handles test running interface + */ + public function action_run() + { + $this->template->body = View::factory('unittest/results'); + + // Get the test suite and work out which groups we're testing + $suite = Kohana_Tests::suite(); + $group = (array) Arr::get($_GET, 'group', array()); + + + // Stop phpunit from interpretting "all groups" as "no groups" + if (empty($group) OR empty($group[0])) + { + $group = array(); + } + + // Only collect code coverage if the user asked for it + $collect_cc = (bool) Arr::get($_GET, 'collect_cc', FALSE); + + if ($collect_cc AND Arr::get($_GET, 'use_whitelist', FALSE)) + { + $whitelist = $this->whitelist(Arr::get($_GET, 'whitelist', array())); + } + + $runner = new Kohana_Unittest_Runner($suite); + + try + { + $runner->run($group, $collect_cc); + + if ($collect_cc) + { + $this->template->body->set('coverage', $runner->calculate_cc_percentage()); + } + + if (isset($whitelist)) + { + $this->template->body->set('coverage_explanation', $this->nice_whitelist_explanation($whitelist)); + } + } + catch(Kohana_Exception $e) + { + // Code coverage is not allowed, possibly xdebug disabled? + // TODO: Tell the user this? + $runner->run($group); + } + + // Show some results + $this->template->body + ->set('results', $runner->results) + ->set('totals', $runner->totals) + ->set('time', $this->nice_time($runner->time)) + + // Sets group to the currently selected group, or default all groups + ->set('group', Arr::get($this->get_groups_list($suite), reset($group), 'All groups')) + ->set('groups', $this->get_groups_list($suite)) + + ->set('report_uri', $this->report_uri.url::query()) + + // Whitelist related stuff + ->set('whitelistable_items', $this->get_whitelistable_items()) + ->set('whitelisted_items', isset($whitelist) ? array_keys($whitelist) : array()) + ->set('whitelist', ! empty($whitelist)); + } + + /** + * Get the list of groups from the test suite, sorted with 'All groups' prefixed + * + * @return array Array of groups in the test suite + */ + protected function get_groups_list($suite) + { + // Make groups aray suitable for drop down + $groups = $suite->getGroups(); + if (count($groups) > 0) + { + sort($groups); + $groups = array_combine($groups, $groups); + } + return array('' => 'All Groups') + $groups; + } + + /** + * Gets a list of items that are whitelistable + * + * @return array + */ + protected function get_whitelistable_items() + { + static $whitelist; + + if (count($whitelist)) + { + return $whitelist; + } + + $whitelist = array(); + + $whitelist['k_app'] = 'Application'; + + $k_modules = array_keys(Kohana::modules()); + + $whitelist += array_map('ucfirst', array_combine($k_modules, $k_modules)); + + $whitelist['k_sys'] = 'Kohana Core'; + + return $whitelist; + } + + /** + * Whitelists a specified set of modules specified by the user + * + * @param array $modules + */ + protected function whitelist(array $modules) + { + $k_modules = Kohana::modules(); + $whitelist = array(); + + // Make sure our whitelist is valid + foreach ($modules as $item) + { + if (isset($k_modules[$item])) + { + $whitelist[$item] = $k_modules[$item]; + } + elseif ($item === 'k_app') + { + $whitelist[$item] = APPPATH; + } + elseif ($item === 'k_sys') + { + $whitelist[$item] = SYSPATH; + } + } + + if (count($whitelist)) + { + Kohana_Tests::whitelist($whitelist); + } + + return $whitelist; + } + + /** + * Prettifies the list of whitelisted modules + * + * @param array Array of whitelisted items + * @return string + */ + protected function nice_whitelist_explanation(array $whitelist) + { + $items = array_intersect_key($this->get_whitelistable_items(), $whitelist); + + return implode(', ', $items); + } + + protected function nice_time($time) + { + $parts = array(); + + if ($time > DATE::DAY) + { + $parts[] = floor($time/DATE::DAY).'d'; + $time = $time % DATE::DAY; + } + + if ($time > DATE::HOUR) + { + $parts[] = floor($time/DATE::HOUR).'h'; + $time = $time % DATE::HOUR; + } + + if ($time > DATE::MINUTE) + { + $parts[] = floor($time/DATE::MINUTE).'m'; + $time = $time % DATE::MINUTE; + } + + if ($time > 0) + { + $parts[] = round($time, 1).'s'; + } + + return implode(' ', $parts); + } +} // End Controller_PHPUnit diff --git a/includes/kohana/modules/unittest/classes/kohana/tests.php b/includes/kohana/modules/unittest/classes/kohana/tests.php new file mode 100644 index 00000000..39fa3640 --- /dev/null +++ b/includes/kohana/modules/unittest/classes/kohana/tests.php @@ -0,0 +1,263 @@ + + * @author Paul Banks + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_Tests +{ + static protected $cache = array(); + + /** + * Loads test files if they cannot be found by kohana + * @param $class + */ + static function autoload($class) + { + $file = str_replace('_', '/', $class); + + if ($file = Kohana::find_file('tests', $file)) + { + require_once $file; + } + } + + /** + * Configures the environment for testing + * + * Does the following: + * + * * Loads the phpunit framework (for the web ui) + * * Restores exception phpunit error handlers (for cli) + * * registeres an autoloader to load test files + */ + static public function configure_environment($do_whitelist = TRUE, $do_blacklist = TRUE) + { + if ( ! class_exists('PHPUnit_Util_Filter', FALSE)) + { + // Make sure the PHPUnit classes are available + require_once 'PHPUnit/Framework.php'; + } + + if (Kohana::$is_cli) + { + restore_exception_handler(); + restore_error_handler(); + } + + spl_autoload_register(array('Kohana_Tests', 'autoload')); + + Kohana_Tests::$cache = ($cache = Kohana::cache('unittest_whitelist_cache')) === NULL ? array() : $cache; + + $config = Kohana::config('unittest'); + + if ($do_whitelist AND $config->use_whitelist) + { + self::whitelist(); + } + + if ($do_blacklist AND count($config['blacklist'])) + { + foreach ($config->blacklist as $item) + { + if (is_dir($item)) + { + PHPUnit_Util_Filter::addDirectoryToFilter($item); + } + else + { + PHPUnit_Util_Filter::addFileToFilter($item); + } + } + } + } + + /** + * Helper function to see if unittest is enabled in the config + * + * @return boolean + */ + static function enabled() + { + $p_environment = Kohana::config('unittest.environment'); + $k_environment = Kohana::$environment; + + return (is_array($p_environment) AND in_array($k_environment, $p_environment)) + OR + ($k_environment === $p_environment); + } + + /** + * Creates the test suite for kohana + * + * @return PHPUnit_Framework_TestSuite + */ + static function suite() + { + static $suite = NULL; + + if ($suite instanceof PHPUnit_Framework_TestSuite) + { + return $suite; + } + + $files = Kohana::list_files('tests'); + + $suite = new PHPUnit_Framework_TestSuite(); + + self::addTests($suite, $files); + + return $suite; + } + + /** + * Add files to test suite $suite + * + * Uses recursion to scan subdirectories + * + * @param PHPUnit_Framework_TestSuite $suite The test suite to add to + * @param array $files Array of files to test + */ + static function addTests(PHPUnit_Framework_TestSuite $suite, array $files) + { + foreach ($files as $file) + { + if (is_array($file)) + { + self::addTests($suite, $file); + } + else + { + // Make sure we only include php files + if (is_file($file) AND substr($file, -strlen(EXT)) === EXT) + { + // The default PHPUnit TestCase extension + if ( ! strpos($file, 'TestCase'.EXT)) + { + $suite->addTestFile($file); + } + else + { + require_once($file); + } + + PHPUnit_Util_Filter::addFileToFilter($file); + } + } + } + } + + /** + * Sets the whitelist + * + * If no directories are provided then the function'll load the whitelist + * set in the config file + * + * @param array $directories Optional directories to whitelist + */ + static public function whitelist(array $directories = NULL) + { + if (empty($directories)) + { + $directories = self::get_config_whitelist(); + } + + if (count($directories)) + { + foreach ($directories as &$directory) + { + $directory = realpath($directory).'/'; + } + + // Only whitelist the "top" files in the cascading filesystem + self::set_whitelist(Kohana::list_files('classes', $directories)); + } + } + + /** + * Works out the whitelist from the config + * Used only on the CLI + * + * @returns array Array of directories to whitelist + */ + static protected function get_config_whitelist() + { + $config = Kohana::config('unittest'); + $directories = array(); + + if ($config->whitelist['app']) + { + $directories['k_app'] = APPPATH; + } + + if ($modules = $config->whitelist['modules']) + { + $k_modules = Kohana::modules(); + + // Have to do this because kohana merges config... + // If you want to include all modules & override defaults then TRUE must be the first + // value in the modules array of your app/config/unittest file + if (array_search(TRUE, $modules, TRUE) === (count($modules) - 1)) + { + $modules = $k_modules; + } + elseif (array_search(FALSE, $modules, TRUE) === FALSE) + { + $modules = array_intersect_key($k_modules, array_combine($modules, $modules)); + } + else + { + // modules are disabled + $modules = array(); + } + + $directories += $modules; + } + + if ($config->whitelist['system']) + { + $directories['k_sys'] = SYSPATH; + } + + return $directories; + } + + /** + * Recursively whitelists an array of files + * + * @param array $files Array of files to whitelist + */ + static protected function set_whitelist($files) + { + foreach ($files as $file) + { + if (is_array($file)) + { + self::set_whitelist($file); + } + else + { + if ( ! isset(Kohana_Tests::$cache[$file])) + { + $relative_path = substr($file, strrpos($file, 'classes'.DIRECTORY_SEPARATOR) + 8, -strlen(EXT)); + $cascading_file = Kohana::find_file('classes', $relative_path); + + // The theory is that if this file is the highest one in the cascading filesystem + // then it's safe to whitelist + Kohana_Tests::$cache[$file] = ($cascading_file === $file); + } + + if (Kohana_Tests::$cache[$file]) + { + PHPUnit_Util_Filter::addFileToWhitelist($file); + } + } + } + } + +} diff --git a/includes/kohana/modules/unittest/classes/kohana/unittest/helpers.php b/includes/kohana/modules/unittest/classes/kohana/unittest/helpers.php new file mode 100644 index 00000000..d20e5f75 --- /dev/null +++ b/includes/kohana/modules/unittest/classes/kohana/unittest/helpers.php @@ -0,0 +1,176 @@ + + * @author Paul Banks + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_Unittest_Helpers +{ + /** + * Static variable used to work out whether we have an internet + * connection + * @see has_internet + * @var boolean + */ + static protected $_has_internet = NULL; + + /** + * Check for internet connectivity + * + * @return boolean Whether an internet connection is available + */ + public static function has_internet() + { + if ( ! isset(self::$_has_internet)) + { + // The @ operator is used here to avoid DNS errors when there is no connection. + $sock = @fsockopen("www.google.com", 80, $errno, $errstr, 1); + + self::$_has_internet = (bool) $sock ? TRUE : FALSE; + } + + return self::$_has_internet; + } + + /** + * Helper function which replaces the "/" to OS-specific delimiter + * + * @param string $path + * @return string + */ + static public function dir_separator($path) + { + return str_replace('/', DIRECTORY_SEPARATOR, $path); + } + + /** + * Removes all cache files from the kohana cache dir + * + * @return void + */ + static public function clean_cache_dir() + { + $cache_dir = opendir(Kohana::$cache_dir); + + while($dir = readdir($cache_dir)) + { + // Cache files are split into directories based on first two characters of hash + if ($dir[0] !== '.' AND strlen($dir) === 2) + { + $dir = self::dir_separator(Kohana::$cache_dir.'/'.$dir.'/'); + + $cache = opendir($dir); + + while($file = readdir($cache)) + { + if ($file[0] !== '.') + { + unlink($dir.$file); + } + } + + closedir($cache); + + rmdir($dir); + } + } + + closedir($cache_dir); + } + + /** + * Backup of the environment variables + * @see set_environment + * @var array + */ + protected $_environment_backup = array(); + + /** + * Allows easy setting & backing up of enviroment config + * + * Option types are checked in the following order: + * + * * Server Var + * * Static Variable + * * Config option + * + * @param array $environment List of environment to set + */ + public function set_environment(array $environment) + { + if ( ! count($environment)) + return FALSE; + + foreach ($environment as $option => $value) + { + $backup_needed = ! array_key_exists($option, $this->_environment_backup); + + // Handle changing superglobals + if (in_array($option, array('_GET', '_POST', '_SERVER', '_FILES'))) + { + // For some reason we need to do this in order to change the superglobals + global $$option; + + if($backup_needed) + { + $this->_environment_backup[$option] = $$option; + } + + // PHPUnit makes a backup of superglobals automatically + $$option = $value; + } + // If this is a static property i.e. Html::$windowed_urls + elseif (strpos($option, '::$') !== FALSE) + { + list($class, $var) = explode('::$', $option, 2); + + $class = new ReflectionClass($class); + + if ($backup_needed) + { + $this->_environment_backup[$option] = $class->getStaticPropertyValue($var); + } + + $class->setStaticPropertyValue($var, $value); + } + // If this is an environment variable + elseif (preg_match('/^[A-Z_-]+$/', $option) OR isset($_SERVER[$option])) + { + if($backup_needed) + { + $this->_environment_backup[$option] = isset($_SERVER[$option]) ? $_SERVER[$option] : ''; + } + + $_SERVER[$option] = $value; + } + // Else we assume this is a config option + else + { + if ($backup_needed) + { + $this->_environment_backup[$option] = Kohana::config($option); + } + + list($group, $var) = explode('.', $option, 2); + + Kohana::config($group)->set($var, $value); + } + } + } + + /** + * Restores the environment to the original state + * + * @chainable + * @return Kohana_Unittest_Helpers $this + */ + public function restore_environment() + { + $this->set_environment($this->_environment_backup); + } +} diff --git a/includes/kohana/modules/unittest/classes/kohana/unittest/runner.php b/includes/kohana/modules/unittest/classes/kohana/unittest/runner.php new file mode 100644 index 00000000..5a5486c7 --- /dev/null +++ b/includes/kohana/modules/unittest/classes/kohana/unittest/runner.php @@ -0,0 +1,304 @@ + + * @author Paul Banks + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +Class Kohana_Unittest_Runner implements PHPUnit_Framework_TestListener +{ + /** + * Results + * @var array + */ + protected $results = array( + 'errors' => array(), + 'failures' => array(), + 'skipped' => array(), + 'incomplete' => array(), + ); + + /** + * Test result totals + * @var array + */ + protected $totals = array( + 'tests' => 0, + 'passed' => 0, + 'errors' => 0, + 'failures' => 0, + 'skipped' => 0, + 'incomplete' => 0, + 'assertions' => 0, + ); + + /** + * Info about the current test running + * @var array + */ + protected $current = array(); + + /** + * Time for tests to run (seconds) + * @var float + */ + protected $time = 0; + + /** + * Result collector + * @var PHPUnit_Framework_TestResult + */ + protected $result = NULL; + + /** + * the test suite to run + * @var PHPUnit_Framework_TestSuite + */ + protected $suite = NULL; + + /** + * Constructor + * + * @param PHPUnit_Framework_TestSuite $suite The suite to test + * @param PHPUnit_Framework_TestResult $result Optional result object to use + */ + function __construct(PHPUnit_Framework_TestSuite $suite, PHPUnit_Framework_TestResult $result = NULL) + { + if ($result === NULL) + { + $result = new PHPUnit_Framework_TestResult; + } + + $result->addListener($this); + + $this->suite = $suite; + $this->result = $result; + } + + /** + * Magic getter to allow access to member variables + * + * @param string $var Variable to get + * @return mixed + */ + function __get($var) + { + return $this->$var; + } + + /** + * Calcualtes stats for each file covered by the code testing + * + * Each member of the returned array is formatted like so: + * + * + * array( + * 'coverage' => $coverage_percent_for_file, + * 'loc' => $lines_of_code, + * 'locExecutable' => $lines_of_executable_code, + * 'locExecuted' => $lines_of_code_executed + * ); + * + * + * @return array Statistics for code coverage of each file + */ + public function calculate_cc() + { + if ($this->result->getCollectCodeCoverageInformation()) + { + $coverage = $this->result->getCodeCoverageInformation(); + + $coverage_summary = PHPUnit_Util_CodeCoverage::getSummary($coverage); + + $stats = array(); + + foreach ($coverage_summary as $file => $_lines) + { + $stats[$file] = PHPUnit_Util_CodeCoverage::getStatistics($coverage_summary, $file); + } + + return $stats; + } + + return FALSE; + } + + /** + * Calculates the percentage code coverage information + * + * @return boolean|float FALSE if cc is not enabled, float for coverage percent + */ + public function calculate_cc_percentage() + { + if ($stats = $this->calculate_cc()) + { + $executable = 0; + $executed = 0; + + foreach ($stats as $stat) + { + $executable += $stat['locExecutable']; + $executed += $stat['locExecuted']; + } + + return $executable > 0 ? ($executed / $executable) * 100 : 100; + } + + return FALSE; + } + + /** + * Generate a report using the specified $temp_path + * + * @param array $groups Groups to test + * @param string $temp_path Temporary path to use while generating report + */ + public function generate_report(array $groups, $temp_path, $create_sub_dir = TRUE) + { + if ( ! is_writable($temp_path)) + { + throw new Kohana_Exception('Temp path :path does not exist or is not writable by the webserver', array(':path' => $temp_path)); + } + + $folder_path = $temp_path; + + if ($create_sub_dir === TRUE) + { + // Icky, highly unlikely, but do it anyway + // Basically adds "(n)" to the end of the filename until there's a free file + $count = 0; + do + { + $folder_name = date('Y-m-d_H:i:s') + .( ! empty($groups) ? '['.implode(',', $groups).']' : '') + .($count > 0 ? '('.$count.')' : ''); + ++$count; + } + while(is_dir($folder_path.$folder_name)); + + $folder_path .= $folder_name; + + mkdir($folder_path, 0777); + } + else + { + $folder_name = basename($folder_path); + } + + $this->run($groups, TRUE); + + require_once 'PHPUnit/Runner/Version.php'; + require_once 'PHPUnit/Util/Report.php'; + + PHPUnit_Util_Report::render($this->result, $folder_path); + + return array($folder_path, $folder_name); + } + + /** + * Runs the test suite using the result specified in the constructor + * + * @param array $groups Optional array of groups to test + * @param bool $collect_cc Optional, Should code coverage be collected? + * @return Kohana_PHPUnit Instance of $this + */ + public function run(array $groups = array(), $collect_cc = FALSE) + { + if ($collect_cc AND ! extension_loaded('xdebug')) + { + throw new Kohana_Exception('Code coverage cannot be collected because the xdebug extension is not loaded'); + } + + $this->result->collectCodeCoverageInformation((bool) $collect_cc); + + // Run the tests. + $this->suite->run($this->result, FALSE, $groups); + + return $this; + } + + public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) + { + $this->totals['errors']++; + $this->current['result'] = 'errors'; + $this->current['message'] = $test->getStatusMessage(); + + } + + public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) + { + $this->totals['failures']++; + $this->current['result'] = 'failures'; + $this->current['message'] = $test->getStatusMessage(); + } + + public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) + { + $this->totals['incomplete']++; + $this->current['result'] = 'incomplete'; + $this->current['message'] = $test->getStatusMessage(); + } + + public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) + { + $this->totals['skipped']++; + $this->current['result'] = 'skipped'; + $this->current['message'] = $test->getStatusMessage(); + } + + public function startTest(PHPUnit_Framework_Test $test) + { + $this->current['name'] = $test->getName(FALSE); + $this->current['description'] = $test->toString(); + $this->current['result'] = 'passed'; + } + + public function endTest(PHPUnit_Framework_Test $test, $time) + { + // Add totals + $this->totals['tests']++; + $this->totals['assertions'] += $test->getNumAssertions(); + + // Handle passed tests + if ($this->current['result'] == 'passed') + { + // Add to total + $this->totals['passed']++; + } + else + { + // Add to results + $this->results[$this->current['result']][] = $this->current; + } + + $this->current = array(); + + $this->time += $time; + } + + public function startTestSuite(PHPUnit_Framework_TestSuite $suite) + { + } + + public function endTestSuite(PHPUnit_Framework_TestSuite $suite) + { + // Parse test descriptions to make them look nicer + foreach ($this->results as $case => $testresults) + { + foreach ($testresults as $type => $result) + { + preg_match('/^(?:([a-z0-9_]+?)::)?([a-z0-9_]+)(?: with data set (#\d+ \(.*?\)))?/i', $result['description'], $m); + + $this->results[$case][$type] += array( + 'class' => $m[1], + 'test' => $m[2], + 'data_set' => isset($m[3]) ? $m[3] : FALSE, + ); + } + } + } +} diff --git a/includes/kohana/modules/unittest/classes/kohana/unittest/testcase.php b/includes/kohana/modules/unittest/classes/kohana/unittest/testcase.php new file mode 100644 index 00000000..f22efdc9 --- /dev/null +++ b/includes/kohana/modules/unittest/classes/kohana/unittest/testcase.php @@ -0,0 +1,102 @@ + + * @author Paul Banks + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +Abstract Class Kohana_Unittest_TestCase extends PHPUnit_Framework_TestCase +{ + /** + * Make sure PHPUnit backs up globals + * @var boolean + */ + protected $backupGlobals = TRUE; + + /** + * A set of unittest helpers that are shared between normal / database + * testcases + * @var Kohana_Unittest_Helpers + */ + protected $_helpers = NULL; + + /** + * A default set of environment to be applied before each test + * @var array + */ + protected $environmentDefault = array(); + + /** + * Creates a predefined environment using the default environment + * + * Extending classes that have their own setUp() should call + * parent::setUp() + */ + public function setUp() + { + $this->_helpers = new Kohana_Unittest_Helpers; + + $this->setEnvironment($this->environmentDefault); + } + + /** + * Restores the original environment overriden with setEnvironment() + * + * Extending classes that have their own tearDown() + * should call parent::tearDown() + */ + public function tearDown() + { + $this->_helpers->restore_environment(); + } + + /** + * Removes all kohana related cache files in the cache directory + */ + public function cleanCacheDir() + { + return Kohana_Unittest_Helpers::clean_cache_dir(); + } + + /** + * Helper function that replaces all occurences of '/' with + * the OS-specific directory separator + * + * @param string $path The path to act on + * @return string + */ + public function dirSeparator($path) + { + return Kohana_Unittest_Helpers::dir_separator($path); + } + + /** + * Allows easy setting & backing up of enviroment config + * + * Option types are checked in the following order: + * + * * Server Var + * * Static Variable + * * Config option + * + * @param array $environment List of environment to set + */ + public function setEnvironment(array $environment) + { + return $this->_helpers->set_environment($environment); + } + + /** + * Check for internet connectivity + * + * @return boolean Whether an internet connection is available + */ + public function hasInternet() + { + return Kohana_Unittest_Helpers::has_internet(); + } +} \ No newline at end of file diff --git a/includes/kohana/modules/unittest/config/unittest.php b/includes/kohana/modules/unittest/config/unittest.php new file mode 100644 index 00000000..b634d8ec --- /dev/null +++ b/includes/kohana/modules/unittest/config/unittest.php @@ -0,0 +1,53 @@ + Kohana::DEVELOPMENT, + + // This is the folder where we generate and zip all the reports for downloading + // Needs to be readable and writable + 'temp_path' => Kohana::$cache_dir.'/unittest', + + // Path from DOCROOT (i.e. http://yourdomain/) to the folder where HTML cc reports can be published. + // If you'd prefer not to allow users to do this then simply set the value to FALSE. + // Example value of 'cc_report_path' would allow devs to see report at http://yourdomain/report/ + 'cc_report_path' => 'report', + + // If you don't use a whitelist then only files included during the request will be counted + // If you do, then only whitelisted items will be counted + 'use_whitelist' => FALSE, + + // Items to whitelist, only used in cli + // Web runner ui allows user to choose which items to whitelist + 'whitelist' => array( + + // Should the app be whitelisted? + // Useful if you just want to test your application + 'app' => TRUE, + + // Set to array(TRUE) to include all modules, or use an array of module names + // (the keys of the array passed to Kohana::modules() in the bootstrap) + // Or set to FALSE to exclude all modules + 'modules' => array(TRUE), + + // If you don't want the Kohana code coverage reports to pollute your app's, + // then set this to FALSE + 'system' => TRUE, + ), + + // Does what it says on the tin + // Blacklisted files won't be included in code coverage reports + // If you use a whitelist then the blacklist will be ignored + 'use_blacklist' => FALSE, + + // List of individual files/folders to blacklist + 'blacklist' => array( + ), + + // A database connection that can be used when testing + // This doesn't overwrite anything, tests will have to use this value manually + 'db_connection' => 'unittest', +); diff --git a/includes/kohana/modules/unittest/example.phpunit.xml b/includes/kohana/modules/unittest/example.phpunit.xml new file mode 100644 index 00000000..9792d37d --- /dev/null +++ b/includes/kohana/modules/unittest/example.phpunit.xml @@ -0,0 +1,16 @@ + + + + + rel/path/to/unittest/tests.php + + + diff --git a/includes/kohana/modules/unittest/guide/menu.unittest.md b/includes/kohana/modules/unittest/guide/menu.unittest.md new file mode 100644 index 00000000..7bbaf989 --- /dev/null +++ b/includes/kohana/modules/unittest/guide/menu.unittest.md @@ -0,0 +1,5 @@ +1. **UnitTest** + - [Testing](unittest.testing) + - [Mock Objects](unittest.mockobjects) + - [Troubleshooting](unittest.troubleshooting) + - [Testing workflows](unittest.testing_workflows) diff --git a/includes/kohana/modules/unittest/guide/unittest.mockobjects.md b/includes/kohana/modules/unittest/guide/unittest.mockobjects.md new file mode 100644 index 00000000..64b2d230 --- /dev/null +++ b/includes/kohana/modules/unittest/guide/unittest.mockobjects.md @@ -0,0 +1,265 @@ +# Mock objects + +Sometimes when writing tests you need to test something that depends on an object being in a certain state. + +Say for example you're testing a model - you want to make sure that the model is running the correct query, but you don't want it to run on a real database server. You can create a mock database connection which responds in the way the model expects, but doesn't actually connect to a physical database. + +PHPUnit has a built in mock object creator which can generate mocks for classes (inc. abstract ones) on the fly. +It creates a class that extends the one you want to mock. You can also tell PHPUnit to override certain functions to return set values / assert that they're called in a specific way. + +## Creating an instance of a mock class + +You create mocks from within testcases using the getMock() function, which is defined in `PHPUnit_Framework_TestCase` like so: + + getMock($originalClassName, $methods = array(), array $arguments = array(), $mockClassName = '', $callOriginalConstructor = TRUE, $callOriginalClone = TRUE, $callAutoload = TRUE) + +`$originalClassName` +: The name of the class that you want to mock + +`$methods` +: The methods of $originalClassName that you want to mock. + You need to tell PHPUnit in advance because PHP doesn't allow you to extend an object once it's been initialised. + +`$arguments` +: An array of arguments to pass to the mock's constructor + +`$mockClassName` +: Allows you to specify the name that will be given to the mock + +`$callOriginalConstructor` +: Should the mock call its parent's constructor automatically? + +`$callOriginalClone` +: Should the mock call its parent's clone method? + +Most of the time you'll only need to use the first two parameters, i.e.: + + $mock = $this->getMock('ORM'); + +`$mock` now contains a mock of ORM and can be handled as though it were a vanilla instance of `ORM` + + $mock = $this->getMock('ORM', array('check')); + +`$mock` now contains a mock of ORM, but this time we're also mocking the check() method. + +## Mocking methods + +Assuming we've created a mock object like so: + + $mock = $this->getMock('ORM', array('check')); + +We now need to tell PHPUnit how to mock the check function when its called. + +### How many times should it be called? + +You start off by telling PHPUnit how many times the method should be called by calling expects() on the mock object: + + $mock->expects($matcher); + +`expects()` takes one argument, an invoker matcher which you can create using factory methods defined in `PHPUnit_Framework_TestCase`: + +#### Possible invoker matchers: + +`$this->any()` +: Returns a matcher that allows the method to be called any number of times + +`$this->never()` +: Returns a matcher that asserts that the method is never called + +`$this->once()` +: Returns a matcher that asserts that the method is only called once + +`$this->atLeastOnce()` +: Returns a matcher that asserts that the method is called at least once + +`$this->exactly($count)` +: Returns a matcher that asserts that the method is called at least `$count` times + +`$this->at($index)` +: Returns a matcher that matches when the method it is evaluated for is invoked at the given $index. + +In our example we want `check()` to be called once on our mock object, so if we update it accordingly: + + $mock = $this->getMock('ORM', array('check')); + + $mock->expects($this->once()); + +### What is the method we're mocking? + +Although we told PHPUnit what methods we want to mock, we haven't actually told it what method these rules we're specifiying apply to. +You do this by calling `method()` on the returned from `expects()`: + + $mock->expects($matcher) + ->method($methodName); + +As you can probably guess, `method()` takes one parameter, the name of the method you're mocking. +There's nothing very fancy about this function. + + $mock = $this->GetMock('ORM', array('check')); + + $mock->expects($this->once()) + ->method('check'); + + +### What parameters should our mock method expect? + +There are two ways to do this, either + +* Tell the method to accept any parameters +* Tell the method to accept a specific set of parameters + +The former can be achieved by calling `withAnyParameters()` on the object returned from `method()` + + $mock->expects($matcher) + ->method($methodName) + ->withAnyParameters(); + +To only allow specific parameters you can use the `with()` method which accepts any number of parameters. +The order in which you define the parameters is the order that it expects them to be in when called. + + $mock->expects($matcher) + ->method($methodName) + ->with($param1, $param2); + +Calling `with()` without any parameters will force the mock method to accept no parameters. + +PHPUnit has a fairly complex way of comparing parameters passed to the mock method with the expected values, which can be summarised like so - + +* If the values are identical, they are equal +* If the values are of different types they are not equal +* If the values are numbers they they are considered equal if their difference is equal to zero (this level of accuracy can be changed) +* If the values are objects then they are converted to arrays and are compared as arrays +* If the values are arrays then any sub-arrays deeper than x levels (default 10) are ignored in the comparision +* If the values are arrays and one contains more than elements that the other (at any depth up to the max depth), then they are not equal + +#### More advanced parameter comparisions + +Sometimes you need to be more specific about how PHPUnit should compare parameters, i.e. if you want to make sure that one of the parameters is an instance of an object, yet isn't necessarily identical to a particular instance. + +In PHPUnit, the logic for validating objects and datatypes has been refactored into "constraint objects". If you look in any of the assertX() methods you can see that they are nothing more than wrappers for associating constraint objects with tests. + +If a parameter passed to `with()` is not an instance of a constraint object (one which extends `PHPUnit_Framework_Constraint`) then PHPUnit creates a new `IsEqual` comparision object for it. + +i.e., the following methods produce the same result: + + ->with('foo', 1); + + ->with($this->equalTo('foo'), $this->equalTo(1)); + +Here are some of the wrappers PHPUnit provides for creating constraint objects: + +`$this->arrayHasKey($key)` +: Asserts that the parameter will have an element with index `$key` + +`$this->attribute(PHPUnit_Framework_Constraint $constraint, $attributeName)` +: Asserts that object attribute `$attributeName` of the parameter will satisfy `$constraint`, where constraint is an instance of a constraint (i.e. `$this->equalTo()`) + +`$this->fileExists()` +: Accepts no parameters, asserts that the parameter is a path to a valid file (i.e. `file_exists() === TRUE`) + +`$this->greaterThan($value)` +: Asserts that the parameter is greater than `$value` + +`$this->anything()` +: Returns TRUE regardless of what the parameter is + +`$this->equalTo($value, $delta = 0, $canonicalizeEOL = FALSE, $ignoreCase = False)` +: Asserts that the parameter is equal to `$value` (same as not passing a constraint object to `with()`) +: `$delta` is the degree of accuracy to use when comparing numbers. i.e. 0 means numbers need to be identical, 1 means numbers can be within a distance of one from each other +: If `$canonicalizeEOL` is TRUE then all newlines in string values will be converted to `\n` before comparision +: If `$ignoreCase` is TRUE then both strings will be converted to lowercase before comparision + +`$this->identicalTo($value)` +: Asserts that the parameter is identical to `$value` + +`$this->isType($type)` +: Asserts that the parameter is of type `$type`, where `$type` is a string representation of the core PHP data types + +`$this->isInstanceOf($className)` +: Asserts that the parameter is an instance of `$className` + +`$this->lessThan($value)` +: Asserts that the parameter is less than `$value` + +`$this->objectHasAttribute($attribute)` +: Asserts that the paramater (which is assumed to be an object) has an attribute `$attribute` + +`$this->matchesRegularExpression($pattern)` +: Asserts that the parameter matches the PCRE pattern `$pattern` (using `preg_match()`) + +`$this->stringContains($string, $ignoreCase = FALSE)` +: Asserts that the parameter contains the string `$string`. If `$ignoreCase` is TRUE then a case insensitive comparision is done + +`$this->stringEndsWith($suffix)` +: Asserts that the parameter ends with `$suffix` (assumes parameter is a string) + +`$this->stringStartsWith($prefix)` +: Asserts that the parameter starts with `$prefix` (assumes parameter is a string) + +`$this->contains($value)` +: Asserts that the parameter contains at least one value that is identical to `$value` (assumes parameter is array or `SplObjectStorage`) + +`$this->containsOnly($type, $isNativeType = TRUE)` +: Asserts that the parameter only contains items of type `$type`. `$isNativeType` should be set to TRUE when `$type` refers to a built in PHP data type (i.e. int, string etc.) (assumes parameter is array) + + +There are more constraint objects than listed here, look in `PHPUnit_Framework_Assert` and `PHPUnit/Framework/Constraint` if you need more constraints. + +If we continue our example, we have the following: + + $mock->expects($this->once()) + ->method('check') + ->with(); + +So far PHPUnit knows that we want the `check()` method to be called once, with no parameters. Now we just need to get it to return something... + +### What should the method return? + +This is the final stage of mocking a method. + +By default PHPUnit can return either + +* A fixed value +* One of the parameters that were passed to it +* The return value of a specified callback + +Specifying a return value is easy, just call `will()` on the object returned by either `method()` or `with()`. + +The function is defined like so: + + public function will(PHPUnit_Framework_MockObject_Stub $stub) + +PHPUnit provides some MockObject stubs out of the box, you can access them via (when called from a testcase): + +`$this->returnValue($value)` +: Returns `$value` when the mocked method is called + +`$this->returnArgument($argumentIndex)` +: Returns the `$argumentIndex`th argument that was passed to the mocked method + +`$this->returnCallback($callback)` +: Returns the value of the callback, useful for more complicated mocking. +: `$callback` should a valid callback (i.e. `is_callable($callback) === TRUE`). PHPUnit will pass the callback all of the parameters that the mocked method was passed, in the same order / argument index (i.e. the callback is invoked by `call_user_func_array()`). +: You can usually create the callback in your testcase, as long as doesn't begin with "test" + +Obviously if you really want to you can create your own MockObject stub, but these three should cover most situations. + +Updating our example gives: + + $mock->expects($this->once()) + ->method('check') + ->with() + ->will($this->returnValue(TRUE)); + + +And we're done! + +If you now call `$mock->check()` the value TRUE should be returned. + +If you don't call a mocked method and PHPUnit expects it to be called then the test the mock was generated for will fail. + + + diff --git a/includes/kohana/modules/unittest/guide/unittest.testing.md b/includes/kohana/modules/unittest/guide/unittest.testing.md new file mode 100644 index 00000000..dccc3352 --- /dev/null +++ b/includes/kohana/modules/unittest/guide/unittest.testing.md @@ -0,0 +1,117 @@ +### From the command line + + $ phpunit --bootstrap=index.php modules/unittest/tests.php + +Of course, you'll need to make sure the path to the tests.php file is correct. If you want you can copy it to a more accessible location + +### From the web + +Just navigate to http://example.com/unittest. You may need to use http://example.com/index.php/unittest if you have not enabled url rewriting in your .htaccess. + +## Writing tests + +If you're writing a test for your application, place it in "application/tests". Similarly, if you're writing a test for a module place it in modules/[modulefolder]/tests + +Rather than tell you how to write tests I'll point you in the direction of the [PHPUnit Manual](http://www.phpunit.de/manual/3.4/en/index.html). One thing you should bear in mind when writing tests is that testcases should extend Kohana_Unittest_Testcase rathr than PHPUnit_Framework_TestCase. + +Here's a taster of some of the cool things you can do with phpunit: + +### Data Providers + +Sometimes you want to be able to run a specific test with different sets of data to try and test every eventuality + +Ordinarily you could use a foreach loop to iterate over an array of test data, however PHPUnit already can take care of this for us rather easily using "Data Providers". A data provider is a function that returns an array of arguments that can be passed to a test. + + assertSame( + $length, + strlen($string) + ); + } + } + +The key thing to notice is the `@dataProvider` tag in the doccomment, this is what tells PHPUnit to use a data provider. The provider prefix is totally optional but it's a nice standard to identify providers. + +For more info see: + +* [Data Providers in PHPUnit 3.2](http://sebastian-bergmann.de/archives/702-Data-Providers-in-PHPUnit-3.2.html) +* [Data Providers](http://www.phpunit.de/manual/3.4/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.data-providers) + + +### Grouping tests + +To allow users to selectively run tests you need to organise your tests into groups. Here's an example test showing how to do this: + + + = 3.4, there is a bug in 3.3 which causes this. + +## Some of my classes aren't getting whitelisted for code coverage even though their module is + +Only the "highest" files in the cascading filesystem are whitelisted for code coverage. + +To test your module's file, remove the higher file from the cascading filesystem by disabling their respective module. + +A good way of testing is to create a "vanilla" testing environment for your module, devoid of anything that isn't required by the module. + +## I get a blank page when trying to generate a code coverage report + +Try the following: + +1. Generate a html report from the command line using `phpunit {bootstrap info} --coverage-html ./report {insert path to tests.php}`. If any error messages show up, fix them and try to generate the report again +2. Increase the php memory limit +3. Make sure that display_errors is set to "on" in your php.ini config file (this value can sometimes be overriden in a .htaccess file) diff --git a/includes/kohana/modules/unittest/init.php b/includes/kohana/modules/unittest/init.php new file mode 100644 index 00000000..54994872 --- /dev/null +++ b/includes/kohana/modules/unittest/init.php @@ -0,0 +1,23 @@ +)') + ->defaults(array( + 'controller' => 'unittest', + 'action' => 'index', + )); diff --git a/includes/kohana/modules/unittest/tests.php b/includes/kohana/modules/unittest/tests.php new file mode 100644 index 00000000..c8398dd8 --- /dev/null +++ b/includes/kohana/modules/unittest/tests.php @@ -0,0 +1,20 @@ + + +
+

PHPUnit for Kohana 3

+
+
+ Run Tests + 'GET'));?> + + + 'run_group'));?> + + + + 'run_collect_cc')) ?> + +
+ + 'run_use_whitelist')) ?> + +
+ + 'run_whitelist', 'multiple' => 'multiple')) ?> +
+ +
+ + + + + +
+ +
+ Code Coverage Reports + +

+ + 'GET')) ?> + + + 'cc_group'));?> + + + 'report_archive')) ?> + + + 'report_use_whitelist')) ?> + +
+ + 'run_whitelist', 'multiple' => 'multiple')) ?> +
+ + + + +
+
+ +

Useful links

+ +
diff --git a/includes/kohana/modules/unittest/views/unittest/layout.php b/includes/kohana/modules/unittest/views/unittest/layout.php new file mode 100644 index 00000000..5132c70a --- /dev/null +++ b/includes/kohana/modules/unittest/views/unittest/layout.php @@ -0,0 +1,255 @@ + + + + PHPUnit for Kohana + + + + + + + + + + + + diff --git a/includes/kohana/modules/unittest/views/unittest/results.php b/includes/kohana/modules/unittest/views/unittest/results.php new file mode 100644 index 00000000..4b0eef71 --- /dev/null +++ b/includes/kohana/modules/unittest/views/unittest/results.php @@ -0,0 +1,83 @@ + + + +
+ +
No tests in group
+ +
Tests Passed
+ + + $tests):?> + + +
+

+ + []

+
    + +
  1. + :: + + + + +
  2. + +
+
+ + +
diff --git a/includes/kohana/modules/userguide/README.md b/includes/kohana/modules/userguide/README.md new file mode 100644 index 00000000..8bf319a2 --- /dev/null +++ b/includes/kohana/modules/userguide/README.md @@ -0,0 +1,16 @@ +Kohana user guide and live API documentation module + +**Note to Contributors** + +Just so you are aware, I am not merging forks of forks. As the owner of a translation language, I expect you to merge the downstream forks into your own. For instance: + + kohana/userguide + -- you/userguide-xx + -- -- person/userguide-xx + +All changes from `person/userguide-xx` must be merged into your own repo, using `git merge` or the fork queue. See [github forking](http://help.github.com/forking/) for more information about how to merge remote repositories, and note that you will need to add a remote for `person/userguide-xx` using: + + git remote add person git://github.com/person/userguide-xx + git pull person master + +Thanks for your help! \ No newline at end of file diff --git a/includes/kohana/modules/userguide/classes/controller/userguide.php b/includes/kohana/modules/userguide/classes/controller/userguide.php new file mode 100644 index 00000000..fb7768bb --- /dev/null +++ b/includes/kohana/modules/userguide/classes/controller/userguide.php @@ -0,0 +1,314 @@ +request->action === 'media') + { + // Do not template media files + $this->auto_render = FALSE; + } + else + { + // Grab the necessary routes + $this->media = Route::get('docs/media'); + $this->guide = Route::get('docs/guide'); + + if (isset($_GET['lang'])) + { + $lang = $_GET['lang']; + + // Load the accepted language list + $translations = array_keys(Kohana::message('userguide', 'translations')); + + if (in_array($lang, $translations)) + { + // Set the language cookie + Cookie::set('userguide_language', $lang, Date::YEAR); + } + + // Reload the page + $this->request->redirect($this->request->uri); + } + + // Set the translation language + I18n::$lang = Cookie::get('userguide_language', Kohana::config('userguide')->lang); + + if (defined('MARKDOWN_PARSER_CLASS')) + { + throw new Kohana_Exception('Markdown parser already registered. Live documentation will not work in your environment.'); + } + + // Use customized Markdown parser + define('MARKDOWN_PARSER_CLASS', 'Kodoc_Markdown'); + + if ( ! class_exists('Markdown', FALSE)) + { + // Load Markdown support + require Kohana::find_file('vendor', 'markdown/markdown'); + } + + // Set the base URL for links and images + Kodoc_Markdown::$base_url = URL::site($this->guide->uri()).'/'; + Kodoc_Markdown::$image_url = URL::site($this->media->uri()).'/'; + } + + parent::before(); + } + + public function action_docs() + { + $page = $this->request->param('page'); + + if ( ! $page) + { + // Redirect to the default page + $this->request->redirect($this->guide->uri(array('page' => Kohana::config('userguide')->default_page))); + } + + $file = $this->file($page); + + if ( ! $file) + { + $this->error(__('Userguide page not found')); + return; + } + + // Set the page title + $this->template->title = $this->title($page); + + // Parse the page contents into the template + $this->template->content = Markdown(file_get_contents($file)); + + // Attach the menu to the template + $this->template->menu = Markdown(file_get_contents($this->file('menu'))); + + // Bind module menu items + $this->template->bind('module_menus', $module_menus); + + // Attach module-specific menu items + $module_menus = array(); + + foreach(Kohana::modules() as $module => $path) + { + if ($file = $this->file('menu.'.$module)) + { + $module_menus[$module] = Markdown(file_get_contents($file)); + } + } + + // Bind the breadcrumb + $this->template->bind('breadcrumb', $breadcrumb); + + // Add the breadcrumb + $breadcrumb = array(); + $breadcrumb[$this->guide->uri()] = __('User Guide'); + $breadcrumb[] = $this->section($page); + $breadcrumb[] = $this->template->title; + } + + public function action_api() + { + // Enable the missing class autoloader + spl_autoload_register(array('Kodoc_Missing', 'create_class')); + + // Get the class from the request + $class = $this->request->param('class'); + + if ($class) + { + try + { + $_class = Kodoc_Class::factory($class); + + if ( ! Kodoc::show_class($_class)) + throw new Exception(__('That class is hidden')); + } + catch (Exception $e) + { + return $this->error(__('API Reference: Class not found.')); + } + + $this->template->title = $class; + + $this->template->content = View::factory('userguide/api/class') + ->set('doc', Kodoc::factory($class)) + ->set('route', $this->request->route); + } + else + { + $this->template->title = __('Table of Contents'); + + $this->template->content = View::factory('userguide/api/toc') + ->set('classes', Kodoc::class_methods()) + ->set('route', $this->request->route); + } + + // Attach the menu to the template + $this->template->menu = Kodoc::menu(); + + // Bind the breadcrumb + $this->template->bind('breadcrumb', $breadcrumb); + + // Get the docs URI + $guide = Route::get('docs/guide'); + + // Add the breadcrumb + $breadcrumb = array(); + $breadcrumb[$this->guide->uri(array('page' => NULL))] = __('User Guide'); + $breadcrumb[$this->request->route->uri()] = $this->title('api'); + $breadcrumb[] = $this->template->title; + } + + public function action_media() + { + // Generate and check the ETag for this file + $this->request->check_cache(sha1($this->request->uri)); + + // Get the file path from the request + $file = $this->request->param('file'); + + // Find the file extension + $ext = pathinfo($file, PATHINFO_EXTENSION); + + // Remove the extension from the filename + $file = substr($file, 0, -(strlen($ext) + 1)); + + if ($file = Kohana::find_file('media', $file, $ext)) + { + // Send the file content as the response + $this->request->response = file_get_contents($file); + } + else + { + // Return a 404 status + $this->request->status = 404; + } + + // Set the proper headers to allow caching + $this->request->headers['Content-Type'] = File::mime_by_ext($ext); + $this->request->headers['Content-Length'] = filesize($file); + $this->request->headers['Last-Modified'] = date('r', filemtime($file)); + } + + // Display an error if a page isn't found + public function error($message) + { + $this->request->status = 404; + $this->template->title = __('User Guide').' - '.__('Error'); + $this->template->content = View::factory('userguide/error',array('message'=>$message)); + $this->template->menu = Kodoc::menu(); + $this->template->breadcrumb = array($this->guide->uri() => __('User Guide'), __('Error')); + } + + public function after() + { + if ($this->auto_render) + { + // Get the media route + $media = Route::get('docs/media'); + + // Add styles + $this->template->styles = array( + $media->uri(array('file' => 'css/print.css')) => 'print', + $media->uri(array('file' => 'css/screen.css')) => 'screen', + $media->uri(array('file' => 'css/kodoc.css')) => 'screen', + $media->uri(array('file' => 'css/shCore.css')) => 'screen', + $media->uri(array('file' => 'css/shThemeKodoc.css')) => 'screen', + ); + + // Add scripts + $this->template->scripts = array( + $media->uri(array('file' => 'js/jquery.min.js')), + $media->uri(array('file' => 'js/kodoc.js')), + $media->uri(array('file' => 'js/shCore.js')), + $media->uri(array('file' => 'js/shBrushPhp.js')), + ); + + // Add languages + $this->template->translations = Kohana::message('userguide', 'translations'); + } + + return parent::after(); + } + + public function file($page) + { + if ( ! ($file = Kohana::find_file('guide', I18n::$lang.'/'.$page, 'md'))) + { + // Use the default file + $file = Kohana::find_file('guide', $page, 'md'); + } + + return $file; + } + + public function section($page) + { + $markdown = $this->_get_all_menu_markdown(); + + if (preg_match('~\*{2}(.+?)\*{2}[^*]+\[[^\]]+\]\('.preg_quote($page).'\)~mu', $markdown, $matches)) + { + return $matches[1]; + } + + return $page; + } + + public function title($page) + { + $markdown = $this->_get_all_menu_markdown(); + + if (preg_match('~\[([^\]]+)\]\('.preg_quote($page).'\)~mu', $markdown, $matches)) + { + // Found a title for this link + return $matches[1]; + } + + return $page; + } + + protected function _get_all_menu_markdown() + { + // Only do this once per request... + static $markdown = ''; + + if (empty($markdown)) + { + // Get core menu items + $file = $this->file('menu'); + + if ($file AND $text = file_get_contents($file)) + { + $markdown .= $text; + } + + // Look in module specific files + foreach(Kohana::modules() as $module => $path) + { + if ($file = $this->file('menu.'.$module) AND $text = file_get_contents($file)) + { + // Concatenate markdown to produce one string containing all menu items + $markdown .="\n".$text; + } + } + } + + return $markdown; + } + +} // End Userguide diff --git a/includes/kohana/modules/userguide/classes/kodoc.php b/includes/kohana/modules/userguide/classes/kodoc.php new file mode 100644 index 00000000..60484b29 --- /dev/null +++ b/includes/kohana/modules/userguide/classes/kodoc.php @@ -0,0 +1,3 @@ +uri(array('class' => $class->class->name)), $class->class->name); + + if (isset($class->tags['package'])) + { + foreach ($class->tags['package'] as $package) + { + if (isset($class->tags['category'])) + { + foreach ($class->tags['category'] as $category) + { + $menu[$package][$category][] = $link; + } + } + else + { + $menu[$package]['Base'][] = $link; + } + } + } + else + { + $menu['[Unknown]']['Base'][] = $link; + } + } + + // Sort the packages + ksort($menu); + + return View::factory('userguide/api/menu') + ->bind('menu', $menu); + } + + /** + * Returns an array of all the classes available, built by listing all files in the classes folder and then trying to create that class. + * + * This means any empty class files (as in complety empty) will cause an exception + * + * @param array array of files, obtained using Kohana::list_files + * @return array an array of all the class names + */ + public static function classes(array $list = NULL) + { + if ($list === NULL) + { + $list = Kohana::list_files('classes'); + } + + $classes = array(); + + foreach ($list as $name => $path) + { + if (is_array($path)) + { + $classes += Kodoc::classes($path); + } + else + { + // Remove "classes/" and the extension + $class = substr($name, 8, -(strlen(EXT))); + + // Convert slashes to underscores + $class = str_replace(DIRECTORY_SEPARATOR, '_', strtolower($class)); + + $classes[$class] = $class; + } + } + + return $classes; + } + + /** + * Get all classes and methods of files in a list. + * + * > I personally don't like this as it was used on the index page. Way too much stuff on one page. It has potential for a package index page though. + * > For example: class_methods( Kohana::list_files('classes/sprig') ) could make a nice index page for the sprig package in the api browser + * > ~bluehawk + * + */ + public static function class_methods(array $list = NULL) + { + $list = Kodoc::classes($list); + + $classes = array(); + + foreach ($list as $class) + { + $_class = new ReflectionClass($class); + + if (stripos($_class->name, 'Kohana') === 0) + { + // Skip the extension stuff stuff + continue; + } + + $methods = array(); + + foreach ($_class->getMethods() as $_method) + { + $declares = $_method->getDeclaringClass()->name; + + if (stripos($declares, 'Kohana') === 0) + { + // Remove "Kohana_" + $declares = substr($declares, 7); + } + + if ($declares === $_class->name) + { + $methods[] = $_method->name; + } + } + + sort($methods); + + $classes[$_class->name] = $methods; + } + + return $classes; + } + + /** + * Parse a comment to extract the description and the tags + * + * @param string the comment retreived using ReflectionClass->getDocComment() + * @return array array(string $description, array $tags) + */ + public static function parse($comment) + { + // Normalize all new lines to \n + $comment = str_replace(array("\r\n", "\n"), "\n", $comment); + + // Remove the phpdoc open/close tags and split + $comment = array_slice(explode("\n", $comment), 1, -1); + + // Tag content + $tags = array(); + + foreach ($comment as $i => $line) + { + // Remove all leading whitespace + $line = preg_replace('/^\s*\* ?/m', '', $line); + + // Search this line for a tag + if (preg_match('/^@(\S+)(?:\s*(.+))?$/', $line, $matches)) + { + // This is a tag line + unset($comment[$i]); + + $name = $matches[1]; + $text = isset($matches[2]) ? $matches[2] : ''; + + switch ($name) + { + case 'license': + if (strpos($text, '://') !== FALSE) + { + // Convert the lincense into a link + $text = HTML::anchor($text); + } + break; + case 'link': + $text = preg_split('/\s+/', $text, 2); + $text = HTML::anchor($text[0], isset($text[1]) ? $text[1] : $text[0]); + break; + case 'copyright': + if (strpos($text, '(c)') !== FALSE) + { + // Convert the copyright sign + $text = str_replace('(c)', '©', $text); + } + break; + case 'throws': + if (preg_match('/^(\w+)\W(.*)$/',$text,$matches)) + { + $text = HTML::anchor(Route::get('docs/api')->uri(array('class' => $matches[1])), $matches[1]).' '.$matches[2]; + } + else + { + $text = HTML::anchor(Route::get('docs/api')->uri(array('class' => $text)), $text); + } + break; + case 'uses': + if (preg_match('/^([a-z_]+)::([a-z_]+)$/i', $text, $matches)) + { + // Make a class#method API link + $text = HTML::anchor(Route::get('docs/api')->uri(array('class' => $matches[1])).'#'.$matches[2], $text); + } + break; + // Don't show @access lines, they are shown elsewhere + case 'access': + continue 2; + } + + // Add the tag + $tags[$name][] = $text; + } + else + { + // Overwrite the comment line + $comment[$i] = (string) $line; + } + } + + // Concat the comment lines back to a block of text + if ($comment = trim(implode("\n", $comment))) + { + // Parse the comment with Markdown + $comment = Markdown($comment); + } + + return array($comment, $tags); + } + + /** + * Get the source of a function + * + * @param string the filename + * @param int start line? + * @param int end line? + */ + public static function source($file, $start, $end) + { + if ( ! $file) + { + return FALSE; + } + + $file = file($file, FILE_IGNORE_NEW_LINES); + + $file = array_slice($file, $start - 1, $end - $start + 1); + + if (preg_match('/^(\s+)/', $file[0], $matches)) + { + $padding = strlen($matches[1]); + + foreach ($file as & $line) + { + $line = substr($line, $padding); + } + } + + return implode("\n", $file); + } + + /** + * Test whether a class should be shown, based on the api_packages config option + * + * @param Kodoc_Class the class to test + * @return bool whether this class should be shown + */ + public static function show_class(Kodoc_Class $class) + { + $api_packages = Kohana::config('userguide.api_packages'); + + // If api_packages is true, all packages should be shown + if ($api_packages === TRUE) + return TRUE; + + // Get the package tags for this class (as an array) + $packages = Arr::get($class->tags,'package',Array('None')); + + $show_this = FALSE; + + // Loop through each package tag + foreach ($packages as $package) + { + // If this package is in the allowed packages, set show this to true + if (in_array($package,explode(',',$api_packages))) + $show_this = TRUE; + } + + return $show_this; + } + + +} // End Kodoc diff --git a/includes/kohana/modules/userguide/classes/kohana/kodoc/class.php b/includes/kohana/modules/userguide/classes/kohana/kodoc/class.php new file mode 100644 index 00000000..1681daff --- /dev/null +++ b/includes/kohana/modules/userguide/classes/kohana/kodoc/class.php @@ -0,0 +1,167 @@ +class = new ReflectionClass($class); + + if ($modifiers = $this->class->getModifiers()) + { + $this->modifiers = ''.implode(' ', Reflection::getModifierNames($modifiers)).' '; + } + + if ($constants = $this->class->getConstants()) + { + foreach ($constants as $name => $value) + { + $this->constants[$name] = Kohana::debug($value); + } + } + + $parent = $this->class; + + do + { + if ($comment = $parent->getDocComment()) + { + // Found a description for this class + break; + } + } + while ($parent = $parent->getParentClass()); + + list($this->description, $this->tags) = Kodoc::parse($comment); + } + + /** + * Gets a list of the class properties as [Kodoc_Property] objects. + * + * @return array + */ + public function properties() + { + $props = $this->class->getProperties(); + + sort($props); + + foreach ($props as $key => $property) + { + // Only show public properties, because Reflection can't get the private ones + if ($property->isPublic()) + { + $props[$key] = new Kodoc_Property($this->class->name, $property->name); + } + else + { + unset($props[$key]); + } + } + + return $props; + } + + /** + * Gets a list of the class properties as [Kodoc_Method] objects. + * + * @return array + */ + public function methods() + { + $methods = $this->class->getMethods(); + + usort($methods, array($this,'_method_sort')); + + foreach ($methods as $key => $method) + { + $methods[$key] = new Kodoc_Method($this->class->name, $method->name); + } + + return $methods; + } + + protected function _method_sort($a,$b) + { + /* + echo kohana::debug('a is '.$a->class.'::'.$a->name,'b is '.$b->class.'::'.$b->name, + 'are the classes the same?',$a->class == $b->class,'if they are, the result is:',strcmp($a->name,$b->name), + 'is a this class?',$a->name == $this->class->name,-1, + 'is b this class?',$b->name == $this->class->name,1, + 'otherwise, the result is:',strcmp($a->class,$b->class) + ); + */ + + + // If both methods are defined in the same class, just compare the method names + if ($a->class == $b->class) + return strcmp($a->name,$b->name); + + // If one of them was declared by this class, it needs to be on top + if ($a->name == $this->class->name) + return -1; + if ($b->name == $this->class->name) + return 1; + + // Otherwise, get the parents of each methods declaring class, then compare which function has more "ancestors" + $adepth = 0; + $bdepth = 0; + + $parent = $a->getDeclaringClass(); + do + { + $adepth++; + } + while ($parent = $parent->getParentClass()); + + $parent = $b->getDeclaringClass(); + do + { + $bdepth++; + } + while ($parent = $parent->getParentClass()); + + return $bdepth - $adepth; + } + +} // End Kodac_Class diff --git a/includes/kohana/modules/userguide/classes/kohana/kodoc/markdown.php b/includes/kohana/modules/userguide/classes/kohana/kodoc/markdown.php new file mode 100644 index 00000000..51530f65 --- /dev/null +++ b/includes/kohana/modules/userguide/classes/kohana/kodoc/markdown.php @@ -0,0 +1,169 @@ +span_gamut['doImageURL'] = 9; + + // doLink is 20, add base url just before + $this->span_gamut['doBaseURL'] = 19; + + // Add API links + $this->span_gamut['doAPI'] = 90; + + // Add note spans last + $this->span_gamut['doNotes'] = 100; + + // Parse Kohana view inclusions at the very end + $this->document_gamut['doIncludeViews'] = 100; + + // PHP4 makes me sad. + parent::MarkdownExtra_Parser(); + } + + public function doIncludeViews($text) + { + if (preg_match_all('/{{([^\s{}]++)}}/', $text, $matches, PREG_SET_ORDER)) + { + $replace = array(); + + foreach ($matches as $set) + { + list($search, $view) = $set; + + try + { + $replace[$search] = View::factory($view)->render(); + } + catch (Exception $e) + { + ob_start(); + + // Capture the exception handler output and insert it instead + Kohana::exception_handler($e); + + $replace[$search] = ob_get_clean(); + } + } + + $text = strtr($text, $replace); + } + + return $text; + } + + /** + * Add the current base url to all local links. + * + * [filesystem](about.filesystem "Optional title") + * + * @param string span text + * @return string + */ + public function doBaseURL($text) + { + // URLs containing "://" are left untouched + return preg_replace('~(?uri(array('class' => $class)).$method, $link); + } + + /** + * Wrap notes in the applicable markup. Notes can contain single newlines. + * + * [!!] Remember the milk! + * + * @param string span text + * @return string + */ + public function doNotes($text) + { + if ( ! preg_match('/^\[!!\]\s*+(.+?)(?=\n{2,}|$)/s', $text, $match)) + { + return $text; + } + + return $this->hashBlock('

'.$match[1].'

'); + } + +} // End Kodoc_Markdown diff --git a/includes/kohana/modules/userguide/classes/kohana/kodoc/method.php b/includes/kohana/modules/userguide/classes/kohana/kodoc/method.php new file mode 100644 index 00000000..d704a8c0 --- /dev/null +++ b/includes/kohana/modules/userguide/classes/kohana/kodoc/method.php @@ -0,0 +1,141 @@ +method = new ReflectionMethod($class, $method); + + $this->class = $parent = $this->method->getDeclaringClass(); + + if ($modifiers = $this->method->getModifiers()) + { + $this->modifiers = ''.implode(' ', Reflection::getModifierNames($modifiers)).' '; + } + + do + { + if ($parent->hasMethod($method) AND $comment = $parent->getMethod($method)->getDocComment()) + { + // Found a description for this method + break; + } + } + while ($parent = $parent->getParentClass()); + + list($this->description, $tags) = Kodoc::parse($comment); + + if ($file = $this->class->getFileName()) + { + $this->source = Kodoc::source($file, $this->method->getStartLine(), $this->method->getEndLine()); + } + + if (isset($tags['param'])) + { + $params = array(); + + foreach ($this->method->getParameters() as $i => $param) + { + $param = new Kodoc_Method_Param(array($this->method->class,$this->method->name),$i); + + if (isset($tags['param'][$i])) + { + preg_match('/^(\S+)(?:\s*(?:\$'.$param->name.'\s*)?(.+))?$/', $tags['param'][$i], $matches); + + $param->type = $matches[1]; + + if (isset($matches[2])) + { + $param->description = $matches[2]; + } + } + $params[] = $param; + } + + $this->params = $params; + + unset($tags['param']); + } + + if (isset($tags['return'])) + { + foreach ($tags['return'] as $return) + { + if (preg_match('/^(\S*)(?:\s*(.+?))?$/', $return, $matches)) + { + $this->return[] = array($matches[1], isset($matches[2]) ? $matches[2] : ''); + } + } + + unset($tags['return']); + } + + $this->tags = $tags; + } + + public function params_short() + { + $out = ''; + $required = TRUE; + $first = TRUE; + foreach ($this->params as $param) + { + if ($required AND $param->default AND $first) + { + $out .= '[ '.$param; + $required = FALSE; + $first = FALSE; + } + elseif ($required AND $param->default) + { + $out .= '[, '.$param; + $required = FALSE; + } + elseif ($first) + { + $out .= $param; + $first = FALSE; + } + else + { + $out .= ', '.$param; + } + } + + if ( ! $required) + { + $out .= '] '; + } + + return $out; + } + +} // End Kodoc_Method \ No newline at end of file diff --git a/includes/kohana/modules/userguide/classes/kohana/kodoc/method/param.php b/includes/kohana/modules/userguide/classes/kohana/kodoc/method/param.php new file mode 100644 index 00000000..8afa72d8 --- /dev/null +++ b/includes/kohana/modules/userguide/classes/kohana/kodoc/method/param.php @@ -0,0 +1,101 @@ +param = new ReflectionParameter($method, $param); + + $this->name = $this->param->name; + + if ($this->param->isDefaultValueAvailable()) + { + $this->default = Kohana::dump($this->param->getDefaultValue()); + } + + if ($this->param->isPassedByReference()) + { + $this->reference = TRUE; + } + + if ($this->param->isOptional()) + { + $this->optional = TRUE; + } + } + + public function __toString() + { + $display = ''; + + if ($this->type) + { + $display .= ''.$this->type.' '; + } + + if ($this->reference) + { + $display .= '& '; + } + + if ($this->description) + { + $display .= '$'.$this->name.' '; + } + else + { + $display .= '$'.$this->name.' '; + } + + if ($this->default) + { + $display .= '= '.$this->default.' '; + } + + return $display; + } + +} // End Kodoc_Method_Param diff --git a/includes/kohana/modules/userguide/classes/kohana/kodoc/missing.php b/includes/kohana/modules/userguide/classes/kohana/kodoc/missing.php new file mode 100644 index 00000000..f7256724 --- /dev/null +++ b/includes/kohana/modules/userguide/classes/kohana/kodoc/missing.php @@ -0,0 +1,36 @@ +getDocComment()); + + $this->description = $description; + + if ($modifiers = $property->getModifiers()) + { + $this->modifiers = ''.implode(' ', Reflection::getModifierNames($modifiers)).' '; + } + + if (isset($tags['var'])) + { + if (preg_match('/^(\S*)(?:\s*(.+?))?$/', $tags['var'][0], $matches)) + { + $this->type = $matches[1]; + + if (isset($matches[2])) + { + $this->description = $matches[2]; + } + } + } + + $this->property = $property; + + if ($property->isStatic()) + { + $this->value = Kohana::debug($property->getValue($class)); + } + } + +} // End Kodoc_Property diff --git a/includes/kohana/modules/userguide/config/userguide.php b/includes/kohana/modules/userguide/config/userguide.php new file mode 100644 index 00000000..aae05699 --- /dev/null +++ b/includes/kohana/modules/userguide/config/userguide.php @@ -0,0 +1,17 @@ + 'about.kohana', + + // Default the userguide language. + 'lang' => 'en-us', + + // Enable the API browser. TRUE or FALSE + 'api_browser' => TRUE, + + // Enable these packages in the API browser. TRUE for all packages, or a string of comma seperated packages, using 'None' for a class with no @package + // Example: 'api_packages' => 'Kohana,Kohana/Database,Kohana/ORM,None', + 'api_packages' => TRUE, +); diff --git a/includes/kohana/modules/userguide/guide/about.conventions.md b/includes/kohana/modules/userguide/guide/about.conventions.md new file mode 100644 index 00000000..bf0acfe1 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/about.conventions.md @@ -0,0 +1,293 @@ +# Conventions + +It is encouraged to follow Kohana's [coding style](http://dev.kohanaframework.org/wiki/kohana2/CodingStyle). This uses [BSD/Allman style](http://en.wikipedia.org/wiki/Indent_style#BSD.2FAllman_style) bracing, among other things. + +## Class Names and File Location {#classes} + +Class names in Kohana follow a strict convention to facilitate [autoloading](using.autoloading). Class names should have uppercase first letters with underscores to separate words. Underscores are significant as they directly reflect the file location in the filesystem. + +The following conventions apply: + +1. CamelCased class names should not be used, except when it is undesirable to create a new directory level. +2. All class file names and directory names are lowercase. +3. All classes should be in the `classes` directory. This may be at any level in the [cascading filesystem](about.filesystem). + +[!!] Unlike Kohana v2.x, there is no separation between "controllers", "models", "libraries" and "helpers". All classes are placed in the "classes/" directory, regardless if they are static "helpers" or object "libraries". You can use whatever kind of class design you want: static, singleton, adapter, etc. + +## Examples + +Remember that in a class, an underscore means a new directory. Consider the following examples: + +Class Name | File Path +----------------------|------------------------------- +Controller_Template | classes/controller/template.php +Model_User | classes/model/user.php +Database | classes/database.php +Database_Query | classes/database/query.php +Form | classes/form.php + +## Coding Standards {#coding_standards} + +In order to produce highly consistent source code, we ask that everyone follow the coding standards as closely as possible. + +### Brackets +Please use [BSD/Allman Style](http://en.wikipedia.org/wiki/Indent_style#BSD.2FAllman_style) bracketing. + +### Naming Conventions + +Kohana uses under_score naming, not camelCase naming. + +#### Classes + + // Controller class, uses Controller_ prefix + class Controller_Apple extends Controller { + + // Model class, uses Model_ prefix + class Model_Cheese extends Model { + + // Regular class + class Peanut { + +When creating an instance of a class, don't use parentheses if you're not passing something on to the constructor: + + // Correct: + $db = new Database; + + // Incorrect: + $db = new Database(); + +#### Functions and Methods + +Functions should be all lowercase, and use under_scores to separate words: + + function drink_beverage($beverage) + { + +#### Variables + +All variables should be lowercase and use under_score, not camelCase: + + // Correct: + $foo = 'bar'; + $long_example = 'uses underscores'; + + // Incorrect: + $weDontWantThis = 'understood?'; + +### Indentation + +You must use tabs to indent your code. Using spaces for tabbing is strictly forbidden. + +Vertical spacing (for multi-line) is done with spaces. Tabs are not good for vertical alignment because different people have different tab widths. + + $text = 'this is a long text block that is wrapped. Normally, we aim for ' + . 'wrapping at 80 chars. Vertical alignment is very important for ' + . 'code readability. Remember that all indentation is done with tabs,' + . 'but vertical alignment should be completed with spaces, after ' + . 'indenting with tabs.'; + +### String concatenation + +Don't put spaces around the concatenation operator: + + // Correct: + $str = 'one'.$var.'two'; + + // Incorrect: + $str = 'one'. $var .'two'; + $str = 'one' . $var . 'two'; + +### Single Line Statements + +Single-line IF statements should only be used when breaking normal execution (e.g. return or continue): + + // Acceptable: + if ($foo == $bar) + return $foo; + + if ($foo == $bar) + continue; + + if ($foo == $bar) + break; + + if ($foo == $bar) + throw new Exception('You screwed up!'); + + // Not acceptable: + if ($baz == $bun) + $baz = $bar + 2; + +### Comparison Operations + +Please use OR and AND for comparison: + + // Correct: + if (($foo AND $bar) OR ($b AND $c)) + + // Incorrect: + if (($foo && $bar) || ($b && $c)) + +Please use elseif, not else if: + + // Correct: + elseif ($bar) + + // Incorrect: + else if($bar) + +### Switch structures + +Each case, break and default should be on a separate line. The block inside a case or default must be indented by 1 tab. + + switch ($var) + { + case 'bar': + case 'foo': + echo 'hello'; + break; + case 1: + echo 'one'; + break; + default: + echo 'bye'; + break; + } + +### Parentheses + +There should be one space after statement name, followed by a parenthesis. The ! (bang) character must have a space on either side to ensure maximum readability. Except in the case of a bang or type casting, there should be no whitespace after an opening parenthesis or before a closing parenthesis. + + // Correct: + if ($foo == $bar) + if ( ! $foo) + + // Incorrect: + if($foo == $bar) + if(!$foo) + if ((int) $foo) + if ( $foo == $bar ) + if (! $foo) + +### Ternaries + +All ternary operations should follow a standard format. Use parentheses around expressions only, not around just variables. + + $foo = ($bar == $foo) ? $foo : $bar; + $foo = $bar ? $foo : $bar; + +All comparisons and operations must be done inside of a parentheses group: + + $foo = ($bar > 5) ? ($bar + $foo) : strlen($bar); + +When separating complex ternaries (ternaries where the first part goes beyond ~80 chars) into multiple lines, spaces should be used to line up operators, which should be at the front of the successive lines: + + $foo = ($bar == $foo) + ? $foo + : $bar; + +### Type Casting + +Type casting should be done with spaces on each side of the cast: + + // Correct: + $foo = (string) $bar; + if ( (string) $bar) + + // Incorrect: + $foo = (string)$bar; + +When possible, please use type casting instead of ternary operations: + + // Correct: + $foo = (bool) $bar; + + // Incorrect: + $foo = ($bar == TRUE) ? TRUE : FALSE; + +When casting type to integer or boolean, use the short format: + + // Correct: + $foo = (int) $bar; + $foo = (bool) $bar; + + // Incorrect: + $foo = (integer) $bar; + $foo = (boolean) $bar; + +### Constants + +Always use uppercase for constants: + + // Correct: + define('MY_CONSTANT', 'my_value'); + $a = TRUE; + $b = NULL; + + // Incorrect: + define('MyConstant', 'my_value'); + $a = True; + $b = null; + +Place constant comparisons at the end of tests: + + // Correct: + if ($foo !== FALSE) + + // Incorrect: + if (FALSE !== $foo) + +This is a slightly controversial choice, so I will explain the reasoning. If we were to write the previous example in plain English, the correct example would read: + + if variable $foo is not exactly FALSE + +And the incorrect example would read: + + if FALSE is not exactly variable $foo + +Since we are reading left to right, it simply doesn't make sense to put the constant first. + +### Comments + +#### One-line comments + +Use //, preferably above the line of code you're commenting on. Leave a space after it and start with a capital. Never use #. + + // Correct + + //Incorrect + // incorrect + # Incorrect + +### Regular expressions + +When coding regular expressions please use PCRE rather than the POSIX flavor. PCRE is considered more powerful and faster. + + // Correct: + if (preg_match('/abc/i'), $str) + + // Incorrect: + if (eregi('abc', $str)) + +Use single quotes around your regular expressions rather than double quotes. Single-quoted strings are more convenient because of their simplicity. Unlike double-quoted strings they don't support variable interpolation nor integrated backslash sequences like \n or \t, etc. + + // Correct: + preg_match('/abc/', $str); + + // Incorrect: + preg_match("/abc/", $str); + +When performing a regular expression search and replace, please use the $n notation for backreferences. This is preferred over \\n. + + // Correct: + preg_replace('/(\d+) dollar/', '$1 euro', $str); + + // Incorrect: + preg_replace('/(\d+) dollar/', '\\1 euro', $str); + +Finally, please note that the $ character for matching the position at the end of the line allows for a following newline character. Use the D modifier to fix this if needed. [More info](http://blog.php-security.org/archives/76-Holes-in-most-preg_match-filters.html). + + $str = "email@example.com\n"; + + preg_match('/^.+@.+$/', $str); // TRUE + preg_match('/^.+@.+$/D', $str); // FALSE diff --git a/includes/kohana/modules/userguide/guide/about.filesystem.md b/includes/kohana/modules/userguide/guide/about.filesystem.md new file mode 100644 index 00000000..c760c3e2 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/about.filesystem.md @@ -0,0 +1,86 @@ +# Cascading Filesystem + +The Kohana filesystem is a heirarchy of directory structure. When a file is +loaded by [Kohana::find_file], it is searched in the following order: + +Application Path +: Defined as `APPPATH` in `index.php`. The default value is `application`. + +Module Paths +: This is set as an associative array using [Kohana::modules] in `APPPATH/bootstrap.php`. + Each of the values of the array will be searched in the order that the modules + are added. + +System Path +: Defined as `SYSPATH` in `index.php`. The default value is `system`. All of the + main or "core" files and classes are defined here. + +Files that are in directories higher up the include path order take precedence +over files of the same name lower down the order, which makes it is possible to +overload any file by placing a file with the same name in a "higher" directory: + +![Cascading Filesystem Infographic](img/cascading_filesystem.png) + +If you have a view file called `welcome.php` in the `APPPATH/views` and +`SYSPATH/views` directories, the one in application will be returned when +`welcome.php` is loaded because it is at the top of the filesystem. + +## Types of Files + +The top level directories of the application, module, and system paths has the following +default directories: + +classes/ +: All classes that you want to [autoload](using.autoloading) should be stored here. + This includes controllers, models, and all other classes. All classes must + follow the [class naming conventions](about.conventions#classes). + +config/ +: Configuration files return an associative array of options that can be + loaded using [Kohana::config]. See [config usage](using.configuration) for + more information. + +i18n/ +: Translation files return an associative array of strings. Translation is + done using the `__()` method. To translate "Hello, world!" into Spanish, + you would call `__('Hello, world!')` with [I18n::$lang] set to "es-es". + See [translation usage](using.translation) for more information. + +messages/ +: Message files return an associative array of strings that can be loaded + using [Kohana::message]. Messages and i18n files differ in that messages + are not translated, but always written in the default language and referred + to by a single key. See [message usage](using.messages) for more information. + +views/ +: Views are plain PHP files which are used to generate HTML or other output. The view file is + loaded into a [View] object and assigned variables, which it then converts + into an HTML fragment. Multiple views can be used within each other. + See [view usage](usings.views) for more information. + +## Finding Files + +The path to any file within the filesystem can be found by calling [Kohana::find_file]: + + // Find the full path to "classes/cookie.php" + $path = Kohana::find_file('classes', 'cookie'); + + // Find the full path to "views/user/login.php" + $path = Kohana::find_file('views', 'user/login'); + + +# Vendor Extensions + +We call extensions that are not specific to Kohana "vendor" extensions. +For example, if you wanted to use [DOMPDF](http://code.google.com/p/dompdf), +you would copy it to `application/vendor/dompdf` and include the DOMPDF +autoloading class: + + require Kohana::find_file('vendor', 'dompdf/dompdf/dompdf_config.inc'); + +Now you can use DOMPDF without loading any more files: + + $pdf = new DOMPDF; + +[!!] If you want to convert views into PDFs using DOMPDF, try the +[PDFView](http://github.com/shadowhand/pdfview) module. diff --git a/includes/kohana/modules/userguide/guide/about.flow.md b/includes/kohana/modules/userguide/guide/about.flow.md new file mode 100644 index 00000000..5e7f6bc3 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/about.flow.md @@ -0,0 +1,73 @@ +# Request Flow + +Every application follows the same flow: + +1. Application starts from `index.php`. +2. The application, module, and system paths are set. +3. Error reporting levels are set. +4. Install file is loaded, if it exists. +5. The [Kohana] class is loaded. +6. The bootstrap file, `APPPATH/bootstrap.php`, is included. +7. [Kohana::init] is called, which sets up error handling, caching, and logging. +8. [Kohana_Config] readers and [Kohana_Log] writers are attached. +9. [Kohana::modules] is called to enable additional modules. + * Module paths are added to the [cascading filesystem](about.filesystem). + * Includes the module `init.php` file, if it exists. + * The `init.php` file can perform additional environment setup, including adding routes. +10. [Route::set] is called multiple times to define the [application routes](using.routing). +11. [Request::instance] is called to start processing the request. + 1. Checks each route that has been set until a match is found. + 2. Creates the controller instance and passes the request to it. + 3. Calls the [Controller::before] method. + 4. Calls the controller action, which generates the request response. + 5. Calls the [Controller::after] method. + * The above 5 steps can be repeated multiple times when using [HMVC sub-requests](about.mvc). +12. The main [Request] response is displayed + +## index.php + +Kohana follows a [front controller] pattern, which means that all requests are sent to `index.php`. This allows a very clean [filesystem](about.filesystem) design. In `index.php`, there are some very basic configuration options available. You can change the `$application`, `$modules`, and `$system` paths and set the error reporting level. + +The `$application` variable lets you set the directory that contains your application files. By default, this is `application`. The `$modules` variable lets you set the directory that contains module files. The `$system` variable lets you set the directory that contains the default Kohana files. + +You can move these three directories anywhere. For instance, if your directories are set up like this: + + www/ + index.php + application/ + modules/ + system/ + +You could move the directories out of the web root: + + application/ + modules/ + system/ + www/ + index.php + +Then you would change the settings in `index.php` to be: + + $application = '../application'; + $modules = '../modules'; + $system = '../system'; + +Now none of the directories can be accessed by the web server. It is not necessary to make this change, but does make it possible to share the directories with multiple applications, among other things. + +[!!] There is a security check at the top of every Kohana file to prevent it from being accessed without using the front controller. However, it is more secure to move the application, modules, and system directories to a location that cannot be accessed via the web. + +### Error Reporting + +By default, Kohana displays all errors, including strict mode warnings. This is set using [error_reporting](http://php.net/error_reporting): + + error_reporting(E_ALL | E_STRICT); + +When you application is live and in production, a more conservative setting is recommended, such as ignoring notices: + + error_reporting(E_ALL & ~E_NOTICE); + +If you get a white screen when an error is triggered, your host probably has disabled displaying errors. You can turn it on again by adding this line just after your `error_reporting` call: + + ini_set('display_errors', TRUE); + +Errors should **always** be displayed, even in production, because it allows you to use [exception and error handling](debugging.errors) to serve a nice error page rather than a blank white screen when an error happens. diff --git a/includes/kohana/modules/userguide/guide/about.install.md b/includes/kohana/modules/userguide/guide/about.install.md new file mode 100644 index 00000000..f3ab2a98 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/about.install.md @@ -0,0 +1,96 @@ +# Installation + +1. Download the latest **stable** release from the [Kohana website](http://kohanaframework.org/). +2. Unzip the downloaded package to create a `kohana` directory. +3. Upload the contents of this folder to your webserver. +4. Open `application/bootstrap.php` and make the following changes: + - Set the default [timezone](http://php.net/timezones) for your application. + - Set the `base_url` in the [Kohana::init] call to reflect the location of the kohana folder on your server. +6. Make sure the `application/cache` and `application/logs` directories are writable by the web server. +7. Test your installation by opening the URL you set as the `base_url` in your favorite browser. + +[!!] Depending on your platform, the installation's subdirs may have lost their permissions thanks to zip extraction. Chmod them all to 755 by running `find . -type d -exec chmod 0755 {} \;` from the root of your Kohana installation. + +You should see the installation page. If it reports any errors, you will need to correct them before continuing. + +![Install Page](img/install.png "Example of install page") + +Once your install page reports that your environment is set up correctly you need to either rename or delete `install.php` in the root directory. You should then see the Kohana welcome page: + +![Welcome Page](img/welcome.png "Example of welcome page") + +## Setting up a production environment + +There are a few things you'll want to do with your application before moving into production. + +1. See the [Configuration page](about.configuration) in the docs. + This covers most of the global settings that would change between environments. + As a general rule, you should enable caching and disable profiling ([Kohana::init] settings) for production sites. + [Route caching](api/Route#cache) can also help if you have a lot of routes. +2. Catch all exceptions in `application/bootstrap.php`, so that sensitive data is cannot be leaked by stack traces. + See the example below which was taken from Shadowhand's [wingsc.com source](http://github.com/shadowhand/wingsc). +3. Turn on APC or some kind of opcode caching. + This is the single easiest performance boost you can make to PHP itself. The more complex your application, the bigger the benefit of using opcode caching. + +[!!] Note: The default bootstrap will set Kohana::$environment = $_ENV['KOHANA_ENV'] if set. Docs on how to supply this variable are available in your web server's documentation (e.g. [Apache](http://httpd.apache.org/docs/1.3/mod/mod_env.html#setenv), [Lighttpd](http://redmine.lighttpd.net/wiki/1/Docs:ModSetEnv#Options), [Nginx](http://wiki.nginx.org/NginxHttpFcgiModule#fastcgi_param)). This is considered better practice than many alternative methods to set Kohana::$enviroment. + + /** + * Set the environment string by the domain (defaults to Kohana::DEVELOPMENT). + */ + Kohana::$environment = ($_SERVER['SERVER_NAME'] !== 'localhost') ? Kohana::PRODUCTION : Kohana::DEVELOPMENT; + /** + * Initialise Kohana based on environment + */ + Kohana::init(array( + 'base_url' => '/', + 'index_file' => FALSE, + 'profile' => Kohana::$environment !== Kohana::PRODUCTION, + 'caching' => Kohana::$environment === Kohana::PRODUCTION, + )); + + /** + * Execute the main request using PATH_INFO. If no URI source is specified, + * the URI will be automatically detected. + */ + $request = Request::instance($_SERVER['PATH_INFO']); + + try + { + // Attempt to execute the response + $request->execute(); + } + catch (Exception $e) + { + if (Kohana::$environment === Kohana::DEVELOPMENT) + { + // Just re-throw the exception + throw $e; + } + + // Log the error + Kohana::$log->add(Kohana::ERROR, Kohana::exception_text($e)); + + // Create a 404 response + $request->status = 404; + $request->response = View::factory('template') + ->set('title', '404') + ->set('content', View::factory('errors/404')); + } + + if ($request->send_headers()->response) + { + // Get the total memory and execution time + $total = array( + '{memory_usage}' => number_format((memory_get_peak_usage() - KOHANA_START_MEMORY) / 1024, 2).'KB', + '{execution_time}' => number_format(microtime(TRUE) - KOHANA_START_TIME, 5).' seconds'); + + // Insert the totals into the response + $request->response = str_replace(array_keys($total), $total, $request->response); + } + + + /** + * Display the request response. + */ + echo $request->response; + diff --git a/includes/kohana/modules/userguide/guide/about.kohana.md b/includes/kohana/modules/userguide/guide/about.kohana.md new file mode 100644 index 00000000..c1ad270f --- /dev/null +++ b/includes/kohana/modules/userguide/guide/about.kohana.md @@ -0,0 +1,18 @@ +# What is Kohana? + +Kohana is an open source, [object oriented](http://wikipedia.org/wiki/Object-Oriented_Programming) [MVC](http://wikipedia.org/wiki/Model–View–Controller "Model View Controller") [web framework](http://wikipedia.org/wiki/Web_Framework) built using [PHP5](http://php.net/manual/intro-whatis "PHP Hypertext Preprocessor") by a team of volunteers that aims to be swift, secure, and small. + +[!!] Kohana is licensed under a [BSD license](http://kohanaframework.org/license), so you can legally use it for any kind of open source, commercial, or personal project. + +## What makes Kohana great? + +Anything can be extended using the unique [filesystem](about.filesystem) design, little or no [configuration](about.configuration) is necessary, [error handling](debugging.errors) helps locate the source of errors quickly, and [debugging](debugging) and [profiling](debugging.profiling) provide insight into the application. + +To help secure your applications, tools for [XSS removal](security.xss), [input validation](security.validation), [signed cookies](security.cookies), [form](security.forms) and [HTML](security.html) generators are all included. The [database](security.database) layer provides protection against [SQL injection](http://wikipedia.org/wiki/SQL_Injection). Of course, all official code is carefully written and reviewed for security. + +## Contribute to the Documentation + +We are working very hard to provide complete documentation. To help improve the guide, please [fork the userguide](http://github.com/kohana/userguide), make your changes, and send a pull request. If you are not familiar with git, you can also submit a [feature request](http://dev.kohanaframework.org/projects/kohana3/issues) (requires registration). + +## Help, I can't find the answer? +If you are having trouble finding an answer here, have a look through the [unofficial wiki](http://kerkness.ca/wiki/doku.php). Your answer may also be found by searching the [forum](http://forum.kohanaphp.com/) or [stackoverflow](http://stackoverflow.com/questions/tagged/kohana) followed by asking your question on either. Additionally, you can chat with the community of developers on the freenode [#kohana](irc://irc.freenode.net/kohana) IRC channel. \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/about.mvc.md b/includes/kohana/modules/userguide/guide/about.mvc.md new file mode 100644 index 00000000..4c9442d5 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/about.mvc.md @@ -0,0 +1,7 @@ +# (Hierarchical) Model View Controller + +Model View Controller (Or MVC for short) is a popular design pattern that separates your data sources (Model) from the presentation/templates (View) and the request logic (Controller). + +It makes it much easier to develop applications as the system is designed to maximise the code reuse, meaning you don't have to write as much! + +[!!] Stub diff --git a/includes/kohana/modules/userguide/guide/about.translation.md b/includes/kohana/modules/userguide/guide/about.translation.md new file mode 100644 index 00000000..00c3f529 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/about.translation.md @@ -0,0 +1,4 @@ +# Translation + +[!!] This article is a stub! + diff --git a/includes/kohana/modules/userguide/guide/about.upgrading.md b/includes/kohana/modules/userguide/guide/about.upgrading.md new file mode 100644 index 00000000..ce11fcf4 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/about.upgrading.md @@ -0,0 +1,290 @@ +# Upgrading from 2.3.x + +Most of Kohana v3 works very differently from Kohana 2.3, here's a list of common gotchas and tips for upgrading. + +## Naming conventions + +The 2.x series differentiated between different 'types' of class (i.e. controller, model etc.) using suffixes. Folders within model / controller folders didn't have any bearing on the name of the class. + +In 3.0 this approach has been scrapped in favour of the Zend framework filesystem conventions, where the name of the class is a path to the class itself, separated by underscores instead of slashes (i.e. `/some/class/file.php` becomes `Some_Class_File`). + +See the [conventions documentation](start.conventions) for more information. + +## Input Library + +The Input Library has been removed from 3.0 in favour of just using `$_GET` and `$_POST`. + +### XSS Protection + +If you need to XSS clean some user input you can use [Security::xss_clean] to sanitise it, like so: + + $_POST['description'] = security::xss_clean($_POST['description']); + +You can also use the [Security::xss_clean] as a filter with the [Validate] library: + + $validation = new Validate($_POST); + + $validate->filter('description', 'Security::xss_clean'); + +### POST & GET + +One of the great features of the Input library was that if you tried to access the value in one of the superglobal arrays and it didn't exist the Input library would return a default value that you could specify i.e.: + + $_GET = array(); + + // $id is assigned the value 1 + $id = Input::instance()->get('id', 1); + + $_GET['id'] = 25; + + // $id is assigned the value 25 + $id = Input::instance()->get('id', 1); + +In 3.0 you can duplicate this functionality using [Arr::get]: + + $_GET = array(); + + // $id is assigned the value 1 + $id = Arr::get($_GET, 'id', 1); + + $_GET['id'] = 42; + + // $id is assigned the value 42 + $id = Arr::get($_GET, 'id', 1); + +## ORM Library + +There have been quite a few major changes in ORM since 2.3, here's a list of the more common upgrading problems. + +### Member variables + +All member variables are now prefixed with an underscore (_) and are no longer accessible via `__get()`. Instead you have to call a function with the name of the property, minus the underscore. + +For instance, what was once `loaded` in 2.3 is now `_loaded` and can be accessed from outside the class via `$model->loaded()`. + +### Relationships + +In 2.3 if you wanted to iterate a model's related objects you could do: + + foreach($model->{relation_name} as $relation) + +However, in the new system this won't work. In version 2.3 any queries generated using the Database library were generated in a global scope, meaning that you couldn't try and build two queries simultaneously. Take for example: + +# TODO: NEED A DECENT EXAMPLE!!!! + +This query would fail as the second, inner query would 'inherit' the conditions of the first one, thus causing pandemonia. +In v3.0 this has been fixed by creating each query in its own scope, however this also means that some things won't work quite as expected. Take for example: + + foreach(ORM::factory('user', 3)->where('post_date', '>', time() - (3600 * 24))->posts as $post) + { + echo $post->title; + } + +[!!] (See [the Database tutorial](tutorials.databases) for the new query syntax) + +In 2.3 you would expect this to return an iterator of all posts by user 3 where `post_date` was some time within the last 24 hours, however instead it'll apply the where condition to the user model and return a `Model_Post` with the joining conditions specified. + +To achieve the same effect as in 2.3 you need to rearrange the structure slightly: + + foreach(ORM::factory('user', 3)->posts->where('post_date', '>', time() - (36000 * 24))->find_all() as $post) + { + echo $post->title; + } + +This also applies to `has_one` relationships: + + // Incorrect + $user = ORM::factory('post', 42)->author; + // Correct + $user = ORM::factory('post', 42)->author->find(); + +### Has and belongs to many relationships + +In 2.3 you could specify `has_and_belongs_to_many` relationships. In 3.0 this functionality has been refactored into `has_many` *through*. + +In your models you define a `has_many` relationship to the other model but then you add a `'through' => 'table'` attribute, where `'table'` is the name of your through table. For example (in the context of posts<>categories): + + $_has_many = array + ( + 'categories' => array + ( + 'model' => 'category', // The foreign model + 'through' => 'post_categories' // The joining table + ), + ); + +If you've set up kohana to use a table prefix then you don't need to worry about explicitly prefixing the table. + +### Foreign keys + +If you wanted to override a foreign key in 2.x's ORM you had to specify the relationship it belonged to, and your new foreign key in the member variable `$foreign_keys`. + +In 3.0 you now define a `foreign_key` key in the relationship's definition, like so: + + Class Model_Post extends ORM + { + $_belongs_to = array + ( + 'author' => array + ( + 'model' => 'user', + 'foreign_key' => 'user_id', + ), + ); + } + +In this example we should then have a `user_id` field in our posts table. + + + +In has_many relationships the `far_key` is the field in the through table which links it to the foreign table and the foreign key is the field in the through table which links "this" model's table to the through table. + +Consider the following setup, "Posts" have and belong to many "Categories" through `posts_sections`. + +| categories | posts_sections | posts | +|------------|------------------|---------| +| id | section_id | id | +| name | post_id | title | +| | | content | + + Class Model_Post extends ORM + { + protected $_has_many = array( + 'sections' => array( + 'model' => 'category', + 'through' => 'posts_sections', + 'far_key' => 'section_id', + ), + ); + } + + Class Model_Category extends ORM + { + protected $_has_many = array ( + 'posts' => array( + 'model' => 'post', + 'through' => 'posts_sections', + 'foreign_key' => 'section_id', + ), + ); + } + + +Obviously the aliasing setup here is a little crazy, but it's a good example of how the foreign/far key system works. + +### ORM Iterator + +It's also worth noting that `ORM_Iterator` has now been refactored into `Database_Result`. + +If you need to get an array of ORM objects with their keys as the object's pk, you need to call [Database_Result::as_array], e.g. + + $objects = ORM::factory('user')->find_all()->as_array('id'); + +Where `id` is the user table's primary key. + +## Router Library + +In version 2 there was a Router library that handled the main request. It let you define basic routes in a `config/routes.php` file and it would allow you to use custom regex for the routes, however it was fairly inflexible if you wanted to do something radical. + +## Routes + +The routing system (now refered to as the request system) is a lot more flexible in 3.0. Routes are now defined in the bootstrap file (`application/bootstrap.php`) and the module init.php (`modules/module_name/init.php`). It's also worth noting that routes are evaluated in the order that they are defined. + +Instead of defining an array of routes you now create a new [Route] object for each route. Unlike in the 2.x series there is no need to map one uri to another. Instead you specify a pattern for a uri, use variables to mark the segments (i.e. controller, method, id). + +For example, in 2.x these regexes: + + $config['([a-z]+)/?(\d+)/?([a-z]*)'] = '$1/$3/$1'; + +Would map the uri `controller/id/method` to `controller/method/id`. In 3.0 you'd use: + + Route::set('reversed','((/(/)))') + ->defaults(array('controller' => 'posts', 'action' => 'index')); + +[!!] Each uri should have be given a unique name (in this case it's `reversed`), the reasoning behind this is explained in [the url tutorial](tutorials.urls). + +Angled brackets denote dynamic sections that should be parsed into variables. Rounded brackets mark an optional section which is not required. If you wanted to only match uris beginning with admin you could use: + + Rouse::set('admin', 'admin(/(/(/)))'); + +And if you wanted to force the user to specify a controller: + + Route::set('admin', 'admin/(/(/))'); + +Also, Kohana does not use any 'default defaults'. If you want Kohana to assume your default action is 'index', then you have to tell it so! You can do this via [Route::defaults]. If you need to use custom regex for uri segments then pass an array of `segment => regex` i.e.: + + Route::set('reversed', '((/(/)))', array('id' => '[a-z_]+')) + ->defaults(array('controller' => 'posts', 'action' => 'index')) + +This would force the `id` value to consist of lowercase alpha characters and underscores. + +### Actions + +One more thing we need to mention is that methods in a controller that can be accessed via the url are now called "actions", and are prefixed with 'action_'. E.g. in the above example, if the user calls `admin/posts/1/edit` then the action is `edit` but the method called on the controller will be `action_edit`. See [the url tutorial](tutorials.urls) for more info. + +## Sessions + +There are no longer any Session::set_flash(), Session::keep_flash() or Session::expire_flash() methods, instead you must use [Session::get_once]. + +## URL Helper + +Only a few things have changed with the url helper - `url::redirect()` has been moved into `$this->request->redirect()` within controllers) and `Request::instance()->redirect()` instead. + +`url::current` has now been replaced with `$this->request->uri()` + +## Valid / Validation + +These two classes have been merged into a single class called `Validate`. + +The syntax has also changed a little for validating arrays: + + $validate = new Validate($_POST); + + // Apply a filter to all items in the arrays + $validate->filter(TRUE, 'trim'); + + // To specify rules individually use rule() + $validate + ->rule('field', 'not_empty') + ->rule('field', 'matches', array('another_field')); + + // To set multiple rules for a field use rules(), passing an array of rules => params as the second argument + $validate->rules('field', array( + 'not_empty' => NULL, + 'matches' => array('another_field') + )); + +The 'required' rule has also been renamed to 'not_empty' for clarity's sake. + +## View Library + +There have been a few minor changes to the View library which are worth noting. + +In 2.3 views were rendered within the scope of the controller, allowing you to use `$this` as a reference to the controller within the view, this has been changed in 3.0. Views now render in an empty scope. If you need to use `$this` in your view you can bind a reference to it using [View::bind]: `$view->bind('this', $this)`. + +It's worth noting, though, that this is *very* bad practice as it couples your view to the controller, preventing reuse. The recommended way is to pass the required variables to the view like so: + + $view = View::factory('my/view'); + + $view->variable = $this->property; + + // OR if you want to chain this + + $view + ->set('variable', $this->property) + ->set('another_variable', 42); + + // NOT Recommended + $view->bind('this', $this); + +Because the view is rendered in an empty scope `Controller::_kohana_load_view` is now redundant. If you need to modify the view before it's rendered (i.e. to add a generate a site-wide menu) you can use [Controller::after]. + + Class Controller_Hello extends Controller_Template + { + function after() + { + $this->template->menu = '...'; + + return parent::after(); + } + } diff --git a/includes/kohana/modules/userguide/guide/de-de/about.conventions.md b/includes/kohana/modules/userguide/guide/de-de/about.conventions.md new file mode 100644 index 00000000..42f02aaa --- /dev/null +++ b/includes/kohana/modules/userguide/guide/de-de/about.conventions.md @@ -0,0 +1,300 @@ +# Regeln + +Es wird dazu ermutigt, dem Kohana [Programmierstil](http://dev.kohanaframework.org/wiki/kohana2/CodingStyle) zu folgen. Dieser benutzt den [Allman/BSD](http://de.wikipedia.org/wiki/Einr%C3%BCckungsstil#Allman_.2F_BSD_.2F_.E2.80.9EEast_Coast.E2.80.9C_.2F_Horstmann)-Stil. + +## Klassennamen und Dateilage {#classes} + +Das automatische Laden von Klassen wird durch ihre strengen Namensregeln ermglicht. Die Klassen beginnen mit einem Grobuchstaben und ihre Wrter werden durch Unterstriche getrennt. Diese sind entscheidend, an welcher Stelle die Klasse im Dateisystem gefunden wird. + +Folgende Regeln gelten: + +1. Binnenversalien (camelCase) sollten nicht benutzt werden, auer wenn eine weitere Ordner-Ebene unerwnscht ist +2. alle Datei- und Verzeichnisnamen in Kleinbuchstaben +3. alle Klassen werden im `classes`-Verzeichnis in jeder Ebene des [Kaskaden-Dateisystem](about.filesystem) zusammengefasst + +[!!] Im Gegensatz zu Kohana v2.x besteht keine Unterteilung zwischen "Controllern", "Models", "Bibliotheken" und "Helfern". Alle Klassen befinden sich im "classes/"-Verzeichnis, unabhngig ob es statische "Helfer" oder Objekt-"Bibliotheken" sind. Man kann irgendeinen Klassen-Aufbau (statische Klasse, Singleton, Adapter) verwenden, den man mag. + +## Beispiele + +Denk daran, dass der Unterstrich in Klassennamen eine tiefere Verzeichnisebene bedeutet. Beachte folgende Beispiele: + +Klassenname | Dateipfad +----------------------|------------------------------- +Controller_Template | classes/controller/template.php +Model_User | classes/model/user.php +Database | classes/database.php +Database_Query | classes/database/query.php +Form | classes/form.php + +## Programmierstil {#coding_standards} + +Um einen sehr konsistenten Quelltext zu produzieren, bitten wir jeden den folgenden Programmierstil so genau wie mglich umzusetzen. + +### Klammerung + +Bitte benutze den den [Allman/BSD](http://de.wikipedia.org/wiki/Einr%C3%BCckungsstil#Allman_.2F_BSD_.2F_.E2.80.9EEast_Coast.E2.80.9C_.2F_Horstmann)-Stil. + +### Namensregeln + +Kohana benutzt fr Namen Unter_striche, keine BinnenVersalien (camelCase). + +#### Klassen + + // Libary + class Beer { + + // Libary extension, uses Kohana_ prefix + class Beer extends Kohana_Beer { + + // Controller class, uses Controller_ prefix + class Controller_Apple extends Controller { + + // Model class, uses Model_ prefix + class Model_Cheese extends Model { + + // Helper class, cf. libary + class peanut { + +Benutze keine Klammern, wenn eine Klasseninstanz erstellt, aber keine Parameter bergibt: + + // Correct: + $db = new Database; + + // Incorrect: + $db = new Database(); + +#### Funktionen und Methoden + +Funktionen sollten kleingeschrieben sein und Unter_striche zur Worttrennung benutzen: + + function drink_beverage($beverage) + { + +#### Variablen + +Alle Variablen sollten ebenfalls kleingeschrieben sein und Unter_striche benutzen, keine BinnenVersalien (camelCase): + + // Correct: + $foo = 'bar'; + $long_example = 'uses underscores'; + + // Incorrect: + $weDontWantThis = 'understood?'; + +### Einrckung + +Du musst zur Einrckung deines Quelltextes Tabulatoren benutzen. Leerzeichen fr Tabellarisierung zu verwenden, ist strengstens verboten. + +Vertikaler Abstand (bei Mehrzeiligkeit) wird mit Leerzeichen gemacht. Tabulatoren sind schlecht fr die vertikale Ausrichtung, weil verschiedene Leute unterschiedliche Tabulatoren-Breiten haben. + + $text = 'this is a long text block that is wrapped. Normally, we aim for ' + . 'wrapping at 80 chars. Vertical alignment is very important for ' + . 'code readability. Remember that all indentation is done with tabs,' + . 'but vertical alignment should be completed with spaces, after ' + . 'indenting with tabs.'; + +### Zeichenkettenverknpfung + +Setze keine Leerzeichen um den Verknpfungsoperator: + + // Correct: + $str = 'one'.$var.'two'; + + // Incorrect: + $str = 'one'. $var .'two'; + $str = 'one' . $var . 'two'; + +### Einzeilige Ausdrcke + +Einzeilige IF-Bedingungen sollten nur bei Anweisungen benutzt werden, die die normale Verarbeitung unterbrechen (z.B. return oder continue): + + // Acceptable: + if ($foo == $bar) + return $foo; + + if ($foo == $bar) + continue; + + if ($foo == $bar) + break; + + if ($foo == $bar) + throw new Exception('You screwed up!'); + + // Not acceptable: + if ($baz == $bun) + $baz = $bar + 2; + +### Vergleichsoperatoren + +Bitte benutze OR and AND: + + // Correct: + if (($foo AND $bar) OR ($b AND $c)) + + // Incorrect: + if (($foo && $bar) || ($b && $c)) + +Bitte benutze elseif, nicht else if: + + // Correct: + elseif ($bar) + + // Incorrect: + else if($bar) + +### Switch structures + +Each case, break and default should be on a separate line. The block inside a case or default must be indented by 1 tab. + + switch ($var) + { + case 'bar': + case 'foo': + echo 'hello'; + break; + case 1: + echo 'one'; + break; + default: + echo 'bye'; + break; + } + +### Parentheses + +There should be one space after statement name, followed by a parenthesis. The ! (bang) character must have a space on either side to ensure maximum readability. Except in the case of a bang or type casting, there should be no whitespace after an opening parenthesis or before a closing parenthesis. + + // Correct: + if ($foo == $bar) + if ( ! $foo) + + // Incorrect: + if($foo == $bar) + if(!$foo) + if ((int) $foo) + if ( $foo == $bar ) + if (! $foo) + +### Ternaries + +All ternary operations should follow a standard format. Use parentheses around expressions only, not around just variables. + + $foo = ($bar == $foo) ? $foo : $bar; + $foo = $bar ? $foo : $bar; + +All comparisons and operations must be done inside of a parentheses group: + + $foo = ($bar > 5) ? ($bar + $foo) : strlen($bar); + +When separating complex ternaries (ternaries where the first part goes beyond ~80 chars) into multiple lines, spaces should be used to line up operators, which should be at the front of the successive lines: + + $foo = ($bar == $foo) + ? $foo + : $bar; + +### Type Casting + +Type casting should be done with spaces on each side of the cast: + + // Correct: + $foo = (string) $bar; + if ( (string) $bar) + + // Incorrect: + $foo = (string)$bar; + +When possible, please use type casting instead of ternary operations: + + // Correct: + $foo = (bool) $bar; + + // Incorrect: + $foo = ($bar == TRUE) ? TRUE : FALSE; + +When casting type to integer or boolean, use the short format: + + // Correct: + $foo = (int) $bar; + $foo = (bool) $bar; + + // Incorrect: + $foo = (integer) $bar; + $foo = (boolean) $bar; + +### Constants + +Always use uppercase for constants: + + // Correct: + define('MY_CONSTANT', 'my_value'); + $a = TRUE; + $b = NULL; + + // Incorrect: + define('MyConstant', 'my_value'); + $a = True; + $b = null; + +Place constant comparisons at the end of tests: + + // Correct: + if ($foo !== FALSE) + + // Incorrect: + if (FALSE !== $foo) + +This is a slightly controversial choice, so I will explain the reasoning. If we were to write the previous example in plain English, the correct example would read: + + if variable $foo is not exactly FALSE + +And the incorrect example would read: + + if FALSE is not exactly variable $foo + +Since we are reading left to right, it simply doesn't make sense to put the constant first. + +### Comments + +#### One-line comments + +Use //, preferably above the line of code you're commenting on. Leave a space after it and start with a capital. Never use #. + + // Correct + + //Incorrect + // incorrect + # Incorrect + +### Regular expressions + +When coding regular expressions please use PCRE rather than the POSIX flavor. PCRE is considered more powerful and faster. + + // Correct: + if (preg_match('/abc/i'), $str) + + // Incorrect: + if (eregi('abc', $str)) + +Use single quotes around your regular expressions rather than double quotes. Single-quoted strings are more convenient because of their simplicity. Unlike double-quoted strings they don't support variable interpolation nor integrated backslash sequences like \n or \t, etc. + + // Correct: + preg_match('/abc/', $str); + + // Incorrect: + preg_match("/abc/", $str); + +When performing a regular expression search and replace, please use the $n notation for backreferences. This is preferred over \\n. + + // Correct: + preg_replace('/(\d+) dollar/', '$1 euro', $str); + + // Incorrect: + preg_replace('/(\d+) dollar/', '\\1 euro', $str); + +Finally, please note that the $ character for matching the position at the end of the line allows for a following newline character. Use the D modifier to fix this if needed. [More info](http://blog.php-security.org/archives/76-Holes-in-most-preg_match-filters.html). + + $str = "email@example.com\n"; + + preg_match('/^.+@.+$/', $str); // TRUE + preg_match('/^.+@.+$/D', $str); // FALSE diff --git a/includes/kohana/modules/userguide/guide/de-de/about.kohana.md b/includes/kohana/modules/userguide/guide/de-de/about.kohana.md new file mode 100644 index 00000000..68af7fb8 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/de-de/about.kohana.md @@ -0,0 +1,15 @@ +# Was ist Kohana? + +Kohana ist ein Open-Source-basiertes, [objektorientiertes](http://de.wikipedia.org/wiki/Objektorientierte_Programmierung) [MVC](http://de.wikipedia.org/wiki/Model_View_Controller "Model View Controller")-[Webframework](http://de.wikipedia.org/wiki/Web_Application_Framework) unter Verwendung von [PHP5](http://php.net/manual/de/intro-whatis "PHP Hypertext Preprocessor"). Es wird von Freiwilligen entwickelt, das darauf abzielt schnell, sicher und schlank zu sein. + +[!!] Kohana ist unter der [BSD-Lizenz](http://kohanaframework.org/license) verffentlicht, so dass man es rechtlich fr alle Arten von Open-Source-, kommerzieller oder privater Projekte nutzen kann. + +## Was macht Kohana besonders? + +Durch den einzigartigen [Dateisystem](about.filesystem)-Aufbau ist alles erweiterbar und man braucht wenige oder keine [Einstellungen](about.configuration) vornehmen. Die [Fehlerbehandlung](debugging.errors) hilft, die Fehlerquelle schnell zu finden, und die [Fehlersuche](debugging) und [Programmanalyse](debugging.profiling) ermglichen einen Einblick in die Anwendung. + +Um die Sicherheit deiner Anwendung zu untersttzen, enthlt Kohana Werkzeuge fr [XSS-Entfernung](security.xss), [Eingabe-berprfung](security.validation), [signierte Cookies](security.cookies), [Formular](security.forms)- und [HTML](security.html)-Erstellung. Die [Datenbank](security.database)-Schicht bietet Schutz vor [SQL-Injection](http://de.wikipedia.org/wiki/SQL-Injection). Natrlich wurde der gesamte offizielle Quelltext sorgfltig geschrieben und auf Sicherheit geprft. + +## Diese Dokumentation ist scheie! + +Wir bemhen uns um eine vollstndige Dokumentation. Wenn eine Frage trotzdem offen bleibt, versuche es beim [inoffiziellen Wiki](http://kerkness.ca/wiki/doku.php). Wenn du etwas zum Handbuch beitragen oder ndern willst, erstelle bitte [eine Kopie](http://github.com/kohana/userguide), bearbeite sie und stelle eine Anfrage zur Zusammenfhrung. Falls du nicht mit git vertraut bist, kannst du auch ein [Feature-Vorschlag](http://dev.kohanaframework.org/projects/kohana3/issues) (Anmeldung erforderlich) machen. \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/de-de/menu.md b/includes/kohana/modules/userguide/guide/de-de/menu.md new file mode 100644 index 00000000..88e7bc8e --- /dev/null +++ b/includes/kohana/modules/userguide/guide/de-de/menu.md @@ -0,0 +1,31 @@ +1. **Erste Schritte** + - [Was ist Kohana?](about.kohana) + - [Regeln und Stil](about.conventions) + - [Model-View-Controller](about.mvc) + - [Kaskaden-Dateisystem](about.filesystem) + - [Anfrageablauf](about.flow) + - [Installation](about.install) + - [Upgrading](about.upgrading) + - [Schnittstellenbersicht](api) +2. **Allgemeine Verwendung** + - [Einstellungen](using.configuration) + - [autom. Klassen-Aufruf](using.autoloading) + - [Views und HTML](using.views) + - [Sessions und Cookies](using.sessions) + - [Nachrichten](using.messages) +3. **Fehlersuche** + - [Quelltext](debugging.code) + - [Fehlerbehandlung](debugging.errors) + - [Programmanalyse](debugging.profiling) +4. **Sicherheit** + - [XSS](security.xss) + - [Validierung](security.validation) + - [Cookies](security.cookies) + - [Datenbank](security.database) +5. **Tutorials** + - [Hallo Welt](tutorials.helloworld) + - [Routen, URLs und Verweise](tutorials.urls) + - [Bereinigte URLs](tutorials.removeindex) + - [Datenbanken](tutorials.databases) + - [ORM](tutorials.orm) + - [Arbeit mit Git](tutorials.git) diff --git a/includes/kohana/modules/userguide/guide/debugging.code.md b/includes/kohana/modules/userguide/guide/debugging.code.md new file mode 100644 index 00000000..f08b2b01 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/debugging.code.md @@ -0,0 +1,18 @@ +# Debugging + +Kohana includes several powerful tools to help you debug your application. + +The most basic of these is [Kohana::debug]. This simple method will display any number of variables, similar to [var_export](http://php.net/var_export) or [print_r](http://php.net/print_r), but using HTML for extra formatting. + + // Display a dump of the $foo and $bar variables + echo Kohana::debug($foo, $bar); + +Kohana also provides a method to show the source code of a particular file using [Kohana::debug_source]. + + // Display this line of source code + echo Kohana::debug_source(__FILE__, __LINE__); + +If you want to display information about your application files without exposing the installation directory, you can use [Kohana::debug_path]: + + // Displays "APPPATH/cache" rather than the real path + echo Kohana::debug_path(APPPATH.'cache'); diff --git a/includes/kohana/modules/userguide/guide/debugging.errors.md b/includes/kohana/modules/userguide/guide/debugging.errors.md new file mode 100644 index 00000000..b0f62455 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/debugging.errors.md @@ -0,0 +1,22 @@ +# Error/Exception Handling + +Kohana provides both an exception handler and an error handler that transforms errors into exceptions using PHP's [ErrorException](http://php.net/errorexception) class. Many details of the error and the internal state of the application is displayed by the handler: + +1. Exception class +2. Error level +3. Error message +4. Source of the error, with the error line highlighted +5. A [debug backtrace](http://php.net/debug_backtrace) of the execution flow +6. Included files, loaded extensions, and global variables + +## Example + +Click any of the links to toggle the display of additional information: + +
{{userguide/examples/error}}
+ +## Disabling Error/Exception Handling + +If you do not want to use the internal error handling, you can disable it when calling [Kohana::init]: + + Kohana::init(array('errors' => FALSE)); diff --git a/includes/kohana/modules/userguide/guide/debugging.profiling.md b/includes/kohana/modules/userguide/guide/debugging.profiling.md new file mode 100644 index 00000000..493dd484 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/debugging.profiling.md @@ -0,0 +1,20 @@ +# Profiling + +Kohana provides a very simple way to display statistics about your application: + +1. Common [Kohana] method calls +2. Requests +3. [Database] queries +4. Average execution times for your application + +## Example + +You can display or collect the current [profiler] statistics at any time: + +
+ +
+ +## Preview + +{{profiler/stats}} \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/es-es/debugging.errors.md b/includes/kohana/modules/userguide/guide/es-es/debugging.errors.md new file mode 100644 index 00000000..aa124880 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/debugging.errors.md @@ -0,0 +1,24 @@ +# Gestión de Errores/Excepciones + +Kohana proporciona un gestor de errores y excepciones que transforma errores en excepciones usando la clase [ErrorException](http://php.net/errorexception) de PHP. Se muestran muchos detalles del error y del estado interno de la aplicación: + +1. Clase de excepción +2. Nivel del error +3. Mensaje de error +4. Fuente del error, con la línea del error resaltada +5. Una [depuración hacia atrás](http://php.net/debug_backtrace) del flujo de ejecución +6. Archivos incluídos, extensiones cargadas y variables globales + +## Ejemplo + +Haz clic en cualquiera de los enlaces para mostrar la información adicional: + +
{{userguide/examples/error}}
+ +## Desactivando la Gestión de Errores/Excepciones + +Si no quieres usar la gestión de errores interna, la puedes desactivar cuando se llama a [Kohana::init]: + +~~~ +Kohana::init(array('errors' => FALSE)); +~~~ diff --git a/includes/kohana/modules/userguide/guide/es-es/debugging.md b/includes/kohana/modules/userguide/guide/es-es/debugging.md new file mode 100644 index 00000000..d277e327 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/debugging.md @@ -0,0 +1,24 @@ +# Depuración + +Kohana incluye varias herramientas útiles que te ayudarán en la depuración de tus aplicaciones. + +La más básica de ellas es [Kohana::debug]. Este método simple mostrará cualquier número de variables, similar a [var_export] o [print_r], pero usando HTML para una mejor visualización. + +~~~ +// Mostrar el contenido de las variables $foo y $bar +echo Kohana::debug($foo, $bar); +~~~ + +Kohana también proporciona un método para mostrar el código fuente de una línea particular usando [Kohana::debug_source]. + +~~~ +// Mostrar esta línea del código +echo Kohana::debug_source(__FILE__, __LINE__); +~~~ + +Si quieres mostrar información sobre los archivos de tu aplicación sin exponer el directorio de instalación, puedes usar [Kohana::debug_path]: + +~~~ +// Mostrar "APPPATH/cache" en vez de la ruta real +echo Kohana::debug_file(APPPATH.'cache'); +~~~ diff --git a/includes/kohana/modules/userguide/guide/es-es/debugging.profiling.md b/includes/kohana/modules/userguide/guide/es-es/debugging.profiling.md new file mode 100644 index 00000000..f38b78ee --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/debugging.profiling.md @@ -0,0 +1,22 @@ +# Análisis de Rendimiento + +Kohana proporciona una forma muy simple de mostrar estadísticas sobre tu aplicación: + +1. Los métodos de [Kohana] más usados +2. Peticiones +3. Consultas a la Base de Datos ([Database]) +4. Tiempo de ejecución media de tu aplicación + +## Ejemplo + +En cualquier momento puedes mostrar o recolectar las estadísticas actuales del analizador ([profiler]): + +~~~ +
+ +
+~~~ + +## Vista previa + +{{profiler/stats}} diff --git a/includes/kohana/modules/userguide/guide/es-es/features.md b/includes/kohana/modules/userguide/guide/es-es/features.md new file mode 100644 index 00000000..e846d3b0 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/features.md @@ -0,0 +1 @@ +Esta página lista las características de Kohana v3 diff --git a/includes/kohana/modules/userguide/guide/es-es/menu.md b/includes/kohana/modules/userguide/guide/es-es/menu.md new file mode 100644 index 00000000..c6eaed2f --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/menu.md @@ -0,0 +1,23 @@ +1. **[Cómo Empezar](start)** + - [Convenciones y Estilos](start.conventions) + - [Instalación](start.installation) + - [Configuración](start.configuration) + - [Modelo Vista Controlador](start.mvc) + - [Sistema de Archivos](start.filesystem) + - [Autocarga](start.autoloading) + - [Proceso de las Peticiones](start.flow) +2. **[Tutoriales](tutorials)** + - [Hola, Mundo](tutorials.helloworld) + - [Rutas, URLs, y Enlaces](tutorials.urls) + - [Bases de Datos](tutorials.databases) +3. **[Seguridad](security)** + - [XSS](security.xss) + - [Validación](security.validation) + - [Cookies](security.cookies) + - [Bases de Datos](security.database) +4. **[Depuración](debugging)** + - [Gestión de Errores](debugging.errors) + - [Análisis de Rendimiento](debugging.profiling) +5. **[Actualizando](upgrading)** + - [Desde 2.3](upgrading.23) +6. **[Explorar API](api)** diff --git a/includes/kohana/modules/userguide/guide/es-es/security.cookies.md b/includes/kohana/modules/userguide/guide/es-es/security.cookies.md new file mode 100644 index 00000000..c4bc0da7 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/security.cookies.md @@ -0,0 +1,3 @@ +# Seguridad en las Cookies + +[!!] inacabado diff --git a/includes/kohana/modules/userguide/guide/es-es/security.database.md b/includes/kohana/modules/userguide/guide/es-es/security.database.md new file mode 100644 index 00000000..2d86664b --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/security.database.md @@ -0,0 +1,3 @@ +# Seguridad en las Bases de Datos + +[!!] inacabado diff --git a/includes/kohana/modules/userguide/guide/es-es/security.md b/includes/kohana/modules/userguide/guide/es-es/security.md new file mode 100644 index 00000000..0b1b50aa --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/security.md @@ -0,0 +1,3 @@ +# Seguridad + +[!!] inacabado diff --git a/includes/kohana/modules/userguide/guide/es-es/security.validation.md b/includes/kohana/modules/userguide/guide/es-es/security.validation.md new file mode 100644 index 00000000..440151fe --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/security.validation.md @@ -0,0 +1,3 @@ +# Validación + +[!!] inacabado diff --git a/includes/kohana/modules/userguide/guide/es-es/security.xss.md b/includes/kohana/modules/userguide/guide/es-es/security.xss.md new file mode 100644 index 00000000..0f16deb1 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/security.xss.md @@ -0,0 +1,15 @@ +# Seguridad en Cross-Site Scripting (XSS) + +El primer paso para prevenir los ataques de [XSS](http://es.wikipedia.org/wiki/Cross-site_scripting) es saber cuando necesitas protegerte a ti mismo. El XSS sólo puede llevarse a cabo cuando se muestra dentro de contenido HTML, muchas veces vía una entrada de formulario o cuando mostramos datos de resultados de la base de datos. Cualquier variable global que contenga información desde el cliente puede ser contaminada. Esto incluye los datos de las variables $_GET, $_POST, y $_COOKIE. + +## Prevención + +Hay unas pocas reglas simples para proteger el HTML tu aplicación del XSS. La primera es usar el método [Security::xss] para limpiar cualquier dato de entrada que venga de una variable global. Si no necesitas HTML en una variable, usa [strip_tags](http://php.net/strip_tags) para remover todas las etiquetas HTML innecesarias del contenido. + +[!!] Si quieres permitir a los usuarios enviar HTML en tu aplicación, es altamente recomendable usar una herramienta de limpieza de HTML como [HTML Purifier](http://htmlpurifier.org/) o [HTML Tidy](http://php.net/tidy). + +La segunda es que siempre debemos escapar los datos cuando se insertan en el HTML. La clase [HTML] proporciona generadores para muchas de las principales etiquetas, incluyendo scripts, hojas de estilo, enlaces, imágenes, y email (mailto). Cualquier contenido no verificado debería escaparse usando [HTML::chars]. + +## Referencias + +* [OWASP XSS Cheat Sheet](http://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet) diff --git a/includes/kohana/modules/userguide/guide/es-es/start.autoloading.md b/includes/kohana/modules/userguide/guide/es-es/start.autoloading.md new file mode 100644 index 00000000..b115565f --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/start.autoloading.md @@ -0,0 +1,17 @@ +# Autocarga + +Kohana aprovecha la habilidad de PHP [autocarga](http://docs.php.net/manual/es/language.oop5.autoload.php). Esto elimina la necesidad de llamar a [include](http://php.net/include) o [require](http://php.net/require) antes de usar una clase. + +Las clases son cargadas usando el método [Kohana::auto_load], el cual hace una simple conversión del nombre de la clase al nombre del archivo: + +1. Las clases son colocadas en el directorio `classes/` del [sistema de archivos](start.filesystem) +2. Cualquier caracter de barra baja es convertido a barra invertida +2. El nombre de archivo es todo en minúsculas + +Cuando llamamos a una clase que no ha sido cargada (por ejemplo: `Session_Cookie`), Kohana buscará en el sistema de archivos usando [Kohana::find_file] un archivo llamado `classes/session/cookie.php`. + +## Autocargadores personalizados + +[!!] El autocargador por defecto es activado en `application/bootstrap.php`. + +Los cargadores de clases adicionales pueden ser añadidos usando [spl_autoload_register](http://php.net/spl_autoload_register). diff --git a/includes/kohana/modules/userguide/guide/es-es/start.configuration.md b/includes/kohana/modules/userguide/guide/es-es/start.configuration.md new file mode 100644 index 00000000..0413403f --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/start.configuration.md @@ -0,0 +1,94 @@ +# Configuración General + +[!!] por hacer, descripción de los beneficios de las propiedades estáticas para la configuración + +## Configuración Principal + +La primera tarea de configuración de cualquier nueva instalación de Kohana es cambiar la configuración de inicio [Kohana::init] en `application/bootstrap.php`. Los datos configurables son: + +`boolean` errors +: ¿Usar el gestor de errores y excepciones interno? (Por defecto `TRUE`) Establecer a `FALSE` para desactivar el gestor de errores y excepciones de Kohana. + +`boolean` profile +: ¿Hacer análisis de rendimiento interno? (Por defecto `TRUE`) Establecer a `FALSE` para desactivarlo. En sitios en producción debería estar desactivado para un mejor rendimiento. + +`boolean` caching +: ¿Cachear la localización de los archivos entre peticiones? (Por defecto `FALSE`) Establecer a `TRUE` para cachear la + ruta absoluta de los archivos. Esto aumenta dramáticamente la velocidad de [Kohana::find_file] y puede muchas veces + tener un impacto dramático en el desempeño. Sólo activar en sitios en producción o para su prueba. + +`string` charset +: Juego de caracteres usado para todas las entradas y salidas. (Por defecto `"utf-8"`) Debería ser un juego de caracteres que sea soportado por [htmlspecialchars](http://php.net/htmlspecialchars) e [iconv](http://php.net/iconv). + +`string` base_url +: URL base de la aplicación. (Por defecto `"/"`) Puede ser una URL completa o parcial. Por ejemplo "http://example.com/kohana/" o sólo "/kohana/" funcionan ambas por igual. + +`string` index_file +: El archivo PHP que inicia la aplicación. (Por defecto `"index.php"`) Establecer a `FALSE` cuando elimines el archivo index con la reescritura de la URL (mod_rewrite y similares). + +`string` cache_dir +: Directorio de la Cache. (Por defecto `"application/cache"`) Debe apuntar a un directorio **escribible**. + +## Configuración de las Cookies + +Hay varias propiedades estáticas en la clase [Cookie] que deberían establecerse, especialmente en sitios en producción. + +`string` salt +: Cadena que es usada para crear [cookies cifradas](security.cookies) + +`integer` expiration +: Tiempo de expiración en segundos + +`string` path +: Ruta URL para restringir dónde pueden ser accedidas las cookies + +`string` domain +: Dominio URL para restringir dónde pueden ser accedidas las cookies + +`boolean` secure +: Permitir que las cookies sólo sean accedidas por HTTPS + +`boolean` httponly +: Permitir que las cookies sólo sean accedidas por HTTP (también desactiva el acceso por Javascript) + +# Archivos de Configuración + +La configuración se establece en archivos PHP planos, del estilo de: + +~~~ + 'value', + 'options' => array( + 'foo' => 'bar', + ), +); +~~~ + +Si el archivo de configuración anterior se llamaba `myconf.php`, puedes acceder a él usando: + +~~~ +$config = Kohana::config('myconf'); +$options = $config['options']; +~~~ + +[Kohana::config] también proporciona una forma corta para acceder a valores individuales del array de configuración usando "rutas con puntos". + +Obtener el array "options": + +~~~ +$options = Kohana::config('myconf.options'); +~~~ + +Obtener el valor de "foo" del array "options": + +~~~ +$foo = Kohana::config('myconf.options.foo'); +~~~ + +Los arrays de configuración también pueden ser accedidos como objetos, si prefieres ese método: + +~~~ +$options = Kohana::config('myconf')->options; +~~~ diff --git a/includes/kohana/modules/userguide/guide/es-es/start.conventions.md b/includes/kohana/modules/userguide/guide/es-es/start.conventions.md new file mode 100644 index 00000000..731f1373 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/start.conventions.md @@ -0,0 +1,26 @@ +# Convenciones + +## Nombre de las clases y la localización del archivo + +Los nombres de las clases en Kohana siguen una forma estricta para facilitar la [autocarga](start.autoloading). + +Los nombres de las clases deben tener la primera letra mayúscula con barra baja para separar palabras. Las barras bajas son significativas ya que directamente reflejan la localización del archivo en el sistema de archivos. + + Clase Archivo + + Controller_Template classes/controller/template.php + Model_User classes/model/user.php + Model_Auth_User classes/model/auth/user.php + Auth classes/auth.php + +Los nombres de las clases del estilo de PrimeraMayuscula no deberían ser usadas. + +Todos los nombres de los archivos de las clases y los directorios van en minúscula. + +Todas las clases deben ir en el directorio `classes`. Esto debe ser así en cualquier nivel del [sistema de archivos en cascada](start.filesystem). + +Kohana 3 no diferencia entre *tipos* de clases de la misma forma en que Kohana 2.x y otros frameworks lo hacen. No hay diferencia entre una clase tipo 'helper' o una de tipo 'library' - en Kohana 3 cualquier clase puede implementar cualquier interface que necesite o ser estática totalmente (estilo helper), o instanciable, o una mezcla (por ejemplo singleton). + +## Estilo de Código + +Se recomienda seguir el estilo de código usado en Kohana. Usamos el [estilo BSD/Allman](http://en.wikipedia.org/wiki/Indent_style#BSD.2FAllman_style). ([Descripción más pormenorizada](http://dev.kohanaphp.com/wiki/kohana2/CodingStyle) del estilo de código preferido por Kohana) diff --git a/includes/kohana/modules/userguide/guide/es-es/start.filesystem.md b/includes/kohana/modules/userguide/guide/es-es/start.filesystem.md new file mode 100644 index 00000000..bee39919 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/start.filesystem.md @@ -0,0 +1,13 @@ +# Sistema de Archivos en Cascada + +El sistema de archivos de Kohana se compone de una única estructura de directorios que es repetida a lo largo de todos los directorios, lo que llamamos la ruta de inclusión, que sigue el orden: + +1. application +2. modules, según el orden en que sean añadidos +3. system + +Los archivos que se encuentran en directorios superiores en la lista de las rutas de inclusión tienen preferencia sobre los archivos del mismo nombre pero que están más abajo, lo cual hace posible sobrecargar cualquier archivo colocando otro archivo con el mismo nombre en un directorio superior: + +![Cascading Filesystem Infographic](img/cascading_filesystem.png) + +Si tiene un archivo de Vista llamado layout.php en los directorios application/views y system/views, será devuelto el que se encuentra bajo application cuando se busque por layout.php ya que se encuentra más arriba en la lista de inclusión ordenada. Si elimina ese archivo de application/views, el que se encuentra en system/views será devuelto cuando lo busquemos. diff --git a/includes/kohana/modules/userguide/guide/es-es/start.flow.md b/includes/kohana/modules/userguide/guide/es-es/start.flow.md new file mode 100644 index 00000000..401973aa --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/start.flow.md @@ -0,0 +1,21 @@ +# Proceso de las Peticiones + +Cada aplicación sigue el siguiente proceso: + +1. La aplicación empieza desde el archivo `index.php` +2. Incluye `APPPATH/bootstrap.php` +3. bootstrap.php llama a [Kohana::modules] con la lista de módulos usados + 1. Genera una matriz con las rutas para el sistema de archivos en cascada + 2. Comprueba cada módulo para ver si tiene un init.php, y si lo tiene, lo carga + * Cada init.php puede definir una serie de rutas a usar, que son cargadas cuando el archivo init.php es incluido +4. [Request::instance] es llamada para procesar la petición + 1. Comprueba cada ruta hasta que se encuentra una coincidencia + 2. Carga el controlador y le pasa la petición + 3. Llama al método [Controller::before] + 4. Llama a la acción del controlador + 5. Llama al método [Controller::after] +5. Muestra la respuesta a la petición ([Request]) + +La acción del controlador puede ser cambiada por el método [Controller::before] en base a los parámetros de la petición. + +[!!] inacabado diff --git a/includes/kohana/modules/userguide/guide/es-es/start.installation.md b/includes/kohana/modules/userguide/guide/es-es/start.installation.md new file mode 100644 index 00000000..6599326e --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/start.installation.md @@ -0,0 +1,19 @@ +# Instalación + +1. Descarga la última versión **estable** de la [web de Kohana](http://kohanaphp.com/) +2. Descomprime el archivo descargado para crear un directorio `kohana` +3. Sube el contenido de esta carpeta a tu servidor +4. Abre `application/bootstrap.php` y haz los cambios siguientes: + - Establece la [zona horaria](http://php.net/timezones) por defecto para tu aplicación + - Establece `base_url` en la llamada a [Kohana::init] para reflejar la localización de la carpeta de kohana en tu servidor +6. Comprueba que los directorios `application/cache` y `application/logs` tienen permisos de escritura para todos con `chmod application/{cache,logs} 0777` +7. Comprueba tu instalación abriendo la url que has establecido en `base_url` en tu navegador favorito + +[!!] Dependiendo de tu plataforma, los subdirectorios de la instalación podrían haber perdido sus permisos debido a la descompresión zip. Para cambiarle los permisos a todos ejecutar `find . -type d -exec chmod 0755 {} \;` desde la raíz de la instalación de Kohana. + +Deberías ver la página de instalación. Si reporta algún error, debes corregirlo antes de continuar. + +![Install Page](img/install.png "Ejemplo de la página de instalación") + +Una vez que la página de instalación reporta que tu entorno está correcto, debes renombrar o borrar el archivo `install.php` del directorio raíz. Entonces deberías ver la página de bienvenida de Kohana (el texto `hello, world!`). + diff --git a/includes/kohana/modules/userguide/guide/es-es/start.md b/includes/kohana/modules/userguide/guide/es-es/start.md new file mode 100644 index 00000000..826102e3 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/start.md @@ -0,0 +1,11 @@ +# ¿Qué es Kohana? + +Kohana es un [framework web](http://es.wikipedia.org/wiki/Framework_para_aplicaciones_web) [MVC](http://es.wikipedia.org/wiki/Modelo_Vista_Controlador "Modelo Vista Controlador") de código abierto y [orientado a objetos](http://es.wikipedia.org/wiki/Programaci%C3%B3n_orientada_a_objetos) realizado para [PHP5](http://docs.php.net/manual/es/intro-whatis.php "Preprocesador de Hipertexto PHP") por un equipo de voluntarios, y destaca por ser rápido, seguro y ligero. + +[!!] Kohana está licenciado bajo una [licencia BSD](http://kohanaphp.com/license), así que puede legalmente usarlo para cualquier tipo de proyecto de código abierto, comercial, o personal. + +## ¿Qué hace potente a Kohana? + +Cualquier cosa puede ser extendida usando el [sistema de archivos](start.filesystem) de diseño único, poco o nada hay que cambiar en la [configuración](start.configuration), la [gestión de errores](debugging.errors) ayuda a localizar los errores de código rápidamente, y la [depuración](debugging) y [profiling](debugging.profiling) informan sobre la aplicación en sí. + +Para que sus aplicaciones sean más seguras, se incluyen herramientas para [eliminar el XSS](security.xss), [validar de los datos de entrada](security.validation), [cookies](security.cookies), [formularios](security.forms) y [HTML](security.html). La capa de la [base de datos](security.database) proporciona protección de [inyección de código SQL](http://es.wikipedia.org/wiki/Inyecci%C3%B3n_SQL). Por supuesto, todo el código oficial ha sido cuidadosamente escrito y revisado pensando en la seguridad. diff --git a/includes/kohana/modules/userguide/guide/es-es/start.mvc.md b/includes/kohana/modules/userguide/guide/es-es/start.mvc.md new file mode 100644 index 00000000..496695b3 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/start.mvc.md @@ -0,0 +1,5 @@ +# Modelo Vista Controlador + +Modelo Vista Controlador (o MVC) es un patrón de diseño popular que separa el origen de tus datos (Modelo) de la presentación/plantillas (Vista) y la lógica de la petición (Controlador). + +Esto hace mucho más fácil desarrollar aplicaciones y el sistema es diseñado para maximizar la reutilización de código, lo que se traduce en que ¡no tendrás que escribir mucho! diff --git a/includes/kohana/modules/userguide/guide/es-es/tutorials.databases.md b/includes/kohana/modules/userguide/guide/es-es/tutorials.databases.md new file mode 100644 index 00000000..14f29252 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/tutorials.databases.md @@ -0,0 +1,3 @@ +# Bases de Datos + +[!!] inacabado diff --git a/includes/kohana/modules/userguide/guide/es-es/tutorials.helloworld.md b/includes/kohana/modules/userguide/guide/es-es/tutorials.helloworld.md new file mode 100644 index 00000000..cf451c79 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/tutorials.helloworld.md @@ -0,0 +1,106 @@ +# Hola, Mundo + +Muchos frameworks proporcionan algún ejemplo de tipo hola mundo, de forma que ¡sería muy grosero por nuestra parte romper esa tradición! + +Empezaremos creando un hola mundo muy básico, y luego lo ampliaremos para seguir con los principios del patrón MVC. + +## Lo esencial + +En primer lugar, tenemos que crear un controlador que kohana usará para manejar la petición + +Crea el archivo `application/classes/controller/hello.php` en tu directorio **application** y copia dentro el siguiente código: + + template->message = 'hello, world!'; + } + } + +`extends Controller_Template` +: Ahora estamos extendiendo el controlador de plantillas, el cual es más conveniente para usar vistas en nuestro controlador. + +`public $template = 'site';` +: El controlador de plantillas necesita conocer que plantilla queremos usar. Automáticamente cargará la vista definida en esta variable y le asignará el objeto de tipo vista. + +`$this->template->message = 'hello, world!';` +: `$this->template` es una referencia al objeto tipo vista de nuestra plantilla del sitio. Lo que estamos haciendo es asignar a una variable de la vista llamada "message" el valor de "hello, world!" + +Ahora intentamos ejecutar nuestro código... + +
{{userguide/examples/hello_world_error}}
+ +Por alguna razón kohana lanza una excepción y no muestra nuestro sorprendente mensaje. + +Si miramos dentro del mensaje de error podemos ver que la librería View no es capaz de encontrar la plantilla de nuestro sitio, probablemente porque no ha sido creada todavía - ¡*ouch*! + +Vamos y creamos el archivo de vista `application/views/site.php` para nuestro mensaje - + + + + We've got a message for you! + + + +

+

We just wanted to say it! :)

+ + + +Si luego refrescamos la página podremos ver el fruto de nuestra labor - + +![hello, world! We just wanted to say it!](img/hello_world_2.png "hello, world! We just wanted to say it!") + +## En resumen + +En este tutorial has aprendido cómo crear un controlador y usar una vista para separar la lógica de la presentación. + +Esto es obviamente una introducción muy básica al trabajo con kohana y no entra de lleno en el potencial que tienes cuando desarrollas aplicaciones con él. diff --git a/includes/kohana/modules/userguide/guide/es-es/tutorials.md b/includes/kohana/modules/userguide/guide/es-es/tutorials.md new file mode 100644 index 00000000..a4363aec --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/tutorials.md @@ -0,0 +1,7 @@ +# Tutoriales + +[!!] inacabado + +- [Hola, Mundo](tutorials.helloworld) +- [Rutas, URLs, y Enlaces](tutorials.urls) +- [Bases de Datos](tutorials.databases) diff --git a/includes/kohana/modules/userguide/guide/es-es/tutorials.urls.md b/includes/kohana/modules/userguide/guide/es-es/tutorials.urls.md new file mode 100644 index 00000000..21c6912a --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/tutorials.urls.md @@ -0,0 +1,3 @@ +# Rutas, URLs, y Enlaces + +[!!] inacabado diff --git a/includes/kohana/modules/userguide/guide/es-es/upgrading.md b/includes/kohana/modules/userguide/guide/es-es/upgrading.md new file mode 100644 index 00000000..07fefeca --- /dev/null +++ b/includes/kohana/modules/userguide/guide/es-es/upgrading.md @@ -0,0 +1,5 @@ +# Actualizando + +Obviamente te gustaría actualizar tu código desde la versión 2 a la 3, y para hacer esta transición más fácil hemos recopilado algunos de los principales cambios desde la versión + +* [Kohana 2.3](upgrading.23) diff --git a/includes/kohana/modules/userguide/guide/features.md b/includes/kohana/modules/userguide/guide/features.md new file mode 100644 index 00000000..25cbb18e --- /dev/null +++ b/includes/kohana/modules/userguide/guide/features.md @@ -0,0 +1 @@ +This page lists the features of Kohana v3 \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/fr-fr/about.autoloading.md b/includes/kohana/modules/userguide/guide/fr-fr/about.autoloading.md new file mode 100644 index 00000000..753bc9eb --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/about.autoloading.md @@ -0,0 +1,17 @@ +# Auto-chargement de classes + +Kohana tire partie de la fonctionnalité PHP d'[auto-chargement de classes](http://php.net/manual/fr/language.oop5.autoload.php) permettant de s'affranchir des inclusions manuelles avec [include](http://de.php.net/manual/fr/function.include.php) ou [require](http://de.php.net/manual/fr/function.require.php). + +Les classes sont chargées via la méthode [Kohana::auto_load], qui à partir du nom d'une classe, retrouve le fichier associé: + +1. Les classes sont placées dans le répertoire `classes/` de l'[arborescence de fichiers](about.filesystem) +2. Les caractères underscore '_' sont convertis en slashes '/' +2. Les noms de fichier doivent être en minuscule + +Lors de l'appel à une classe non chargée (eg: `Session_Cookie`), Kohana recherchera dans son arboresence via la méthode [Kohana::find_file] le fichier `classes/session/cookie.php`. + +## Auto-chargement tiers + +[!!] Le mécanisme par défaut d'auto-chargement de classes est défini dans le fichier `application/bootstrap.php`. + +Des mécanismes d'auto-chargement supplémentaires peuvent être ajoutés en utilisant [spl_autoload_register](http://php.net/spl_autoload_register). \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/fr-fr/about.configuration.md b/includes/kohana/modules/userguide/guide/fr-fr/about.configuration.md new file mode 100644 index 00000000..0ac81c6f --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/about.configuration.md @@ -0,0 +1,95 @@ +# Configuration Générale + +[!!] todo, description of benefits of static properties for configuration + +## Configuration du noyau + +La toute première configuration à modifier lors d'une installation de kohana est de changer les paramètres d'initlalisation [Kohana::init] dans le fichier `application/bootstrap.php`. Ces paramètres sont: + +`boolean` errors +: Utilisation de la gestion des erreurs et des exceptions? (Défaut `TRUE`) Affecter à `FALSE` pour désactiver + la gestion des erreurs et exceptions. + +`boolean` profile +: Activer le benchmarking interne? (Défault `TRUE`) Affecter à `FALSE` pour désactiver le benchmarking interne. + A desactiver en production pour obtenir de meilleures performances. + +`boolean` caching +: Mettre en cache les chemins des fichiers entre les requêtes? (Défault `FALSE`) Affecter à `TRUE` pour mettre en cache + les chemins absolus. Ceci peut améliorer drastiquement les performances de la méthode [Kohana::find_file]. + +`string` charset +: Jeu de caractères à utiliser pour toutes les entrées et sorties. (Défault `"utf-8"`) Affecter un jeu de caractères supporté aussi bien par [htmlspecialchars](http://fr.php.net/htmlspecialchars) que [iconv](http://fr.php.net/iconv). + +`string` base_url +: URL racine de l'application. (Défault `"/"`) Peut être une URL complète ou partielle. Par exemple "http://example.com/kohana/" ou "/kohana/" fonctionneraient. + +`string` index_file +: Le fichier PHP qui démarre l'application. (Défault `"index.php"`) Affecter à `FALSE` pour enlever le fichier index de l'URL en utilisant l'URL Rewriting. + +`string` cache_dir +: Répertoire de stockage du cache. (Défault `"application/cache"`) Doit pointer vers un répertoire **inscriptible**. + +## Paramètres des Cookies + +Il y a plusieurs propriétés statiques dans la classe [Cookie] qui doivent être paramétrées, particuliérement sur les sites en production. + +`string` salt +: La chaîne d'aléa (salt) unique utilisée pour [signer les cookies](security.cookies) + +`integer` expiration +: La durée d'expiration par défaut + +`string` path +: Restreindre l'accès aux cookies par rapport au chemin spécifié + +`string` domain +: Restreindre l'accès aux cookies par rapport au domaine spécifié + +`boolean` secure +: N'autoriser les cookies qu'en HTTPS + +`boolean` httponly +: N'autorise l'accès aux cookies que via HTTP (désactive aussi l'accès javascript) + +# Fichiers de configuration + +La configuration de Kohana est faite dans des fichiers à plat PHP, qui ressemblent à l'exemple ci-dessous: + +~~~ + 'value', + 'options' => array( + 'foo' => 'bar', + ), +); +~~~ + +Supposons que le fichier ci-dessus soit appelé `myconf.php`, il est alors possible d'y accèder de la manière suivante: + +~~~ +$config = Kohana::config('myconf'); +$options = $config['options']; +~~~ + +[Kohana::config] fournit aussi un raccourci pour accèder à des clés spécifiques des tableaux de configuration en utilisant des chemins spérarés par le caractère point. + +Récupérer le tableau "options": + +~~~ +$options = Kohana::config('myconf.options'); +~~~ + +Récupérer la valeur de la clé "foo" du tableau "options": + +~~~ +$foo = Kohana::config('myconf.options.foo'); +~~~ + +Les tableaux de configuration peuvent aussi être parcourus comme des objets comme suit: + +~~~ +$options = Kohana::config('myconf')->options; +~~~ diff --git a/includes/kohana/modules/userguide/guide/fr-fr/about.conventions.md b/includes/kohana/modules/userguide/guide/fr-fr/about.conventions.md new file mode 100644 index 00000000..2ac39c69 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/about.conventions.md @@ -0,0 +1,26 @@ +# Conventions et style de codage + +## Nom de classe et emplacement des fichiers + +Les noms de classe dans Kohana suivent des règles strictes pour faciliter l'[auto-chargement de classes](about.autoloading). + +Ils doivent avoir la première lettre en majuscule, et les mots doivent être séparés par des underscores. Les underscores sont très importants car ils déterminent le chemin d'accès au fichier. + +Nom de classe | Chemin +----------------------|------------------------------- +Controller_Template | classes/controller/template.php +Model_User | classes/model/user.php +Database | classes/database.php +Database_Query | classes/database/query.php + +Les noms de classe ne doivent pas utiliser de syntaxe CamelCase sauf si vous ne souhaitez pas créer un nouveau niveau de répertoire. + +Tous les noms de fichier et répertoire sont en minuscule. + +Toutes les classes doivent être dans le répertoire `classes`. Elles peuvent néanmoins être sur plusieurs niveaux de répertoire de l'[arborescence](about.filesystem). + +Kohana 3 ne différencie pas les *types* de classe comme le fait Kohana 2.x. Il n'y a pas de distinction entre une classe 'helper' ou une 'librairie' – avec Kohana 3 toute classe peut implémenter l'interface que vous souhaitez, qu'elle soit statique (helper), instanciable, ou mixte (e.g. singleton). + +## Style de codage + +Il est vivement conseillé de suivre les [styles de codage](http://dev.kohanaphp.com/wiki/kohana2/CodingStyle) de Kohana c'est-à-dire le [style BSD/Allman](http://en.wikipedia.org/wiki/Indent_style#BSD.2FAllman_style) pour les accolades, entre autres choses. \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/fr-fr/about.filesystem.md b/includes/kohana/modules/userguide/guide/fr-fr/about.filesystem.md new file mode 100644 index 00000000..2a306300 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/about.filesystem.md @@ -0,0 +1,13 @@ +# Arborescence de fichiers en cascade + +L'arborescence de fichiers de Kohana est construite autour d'une structure de répertoires unique qui est dupliquée dans tous les répertoires formant ce que l'on appelle l'"include path". Cette structure est composée des répertoires suivants et dans cet ordre: + +1. application +2. modules, dans l'ordre dans lequel ils ont été ajoutés +3. system + +Les fichiers qui sont dans les répertoires les plus haut de l'"include path" sont prioritaires par rapport aux fichiers de même noms dans des répertoires plus bas. Cela rend possible la surcharge de nimporte quel fichier en plaçant un fichier de même nom dans un répertoire de niveau supérieur: + +![Cascading Filesystem Infographic](img/cascading_filesystem.png) + +Par exemple, si vous avez un fichier appelé layout.php dans les répertoires application/views et system/views, alors celui contenu dans le répertoire application sera retourné lors de l'appel à layout.php du fait qu'il est plus haut dans l'"include path". Si vous supprimez le fichier de application/views, alors c'est celui contenu dans system/views qui sera alors retourné. \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/fr-fr/about.flow.md b/includes/kohana/modules/userguide/guide/fr-fr/about.flow.md new file mode 100644 index 00000000..6f13cb7b --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/about.flow.md @@ -0,0 +1,21 @@ +# Processus de traitement des requêtes + +Toutes les applications suivent le même processus: + +1. L'application commence depuis `index.php` +2. Inclut `APPPATH/bootstrap.php` +3. L'initialisation (bootstrap) appelle [Kohana::modules] avec une liste de modules à utiliser + 1. Génére un tableau de chemins utilisés par l'arborescence en cascade + 2. Vérifie la présence du fichier init.php dans chaque module. Si il existe + * Chaque fichier init.php peut définir un ensemble de routes à utiliser, elles sont chargées lorsque le fichier init.php est inclut +4. [Request::instance] est appelé pour traiter la requête + 1. Vérifie toutes les routes jusqu'à ce que l'une d'entres elles concorde + 2. Charge le controleur et lui transmet la requête + 3. Appelle la méthode [Controller::before] + 4. Appelle l'action du controleur + 5. Appelle la méthode [Controller::after] +5. Affiche la réponse à la requête + +L'action du controleur peut etre changée suivant ses paramètres de la par [Controller::before]. + +[!!] Stub diff --git a/includes/kohana/modules/userguide/guide/fr-fr/about.install.md b/includes/kohana/modules/userguide/guide/fr-fr/about.install.md new file mode 100644 index 00000000..86e533c1 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/about.install.md @@ -0,0 +1,20 @@ +# Installation + +1. Téléchargez la dernière version **stable** depuis [le site web Kohana](http://kohanaphp.com/) +2. Dézippez l'archive téléchargée pour créer le répertoire `kohana` +3. Uploadez le contenu de ce répertoire sur votre serveur web +4. Ouvrez `application/bootstrap.php` et effectuez les changements suivants: + - Affecter la [timezone](http://php.net/timezones) par défaut de votre application + - Affecter `base_url` dans l'appel à [Kohana::init] afin de faire comprendre à votre serveur ou est situé le répertoire kohana uploadé à l'étape précédente +6. Vérifiez que les répertoires `application/cache` et `application/logs` sont inscriptibles en tapant la commande `chmod application/{cache,logs} 0777` (Linux). +7. Testez votre installation en tapant l'URL que vous avez spécifiée dans `base_url` dans votre navigateur préféré + +[!!] Suivant votre plateforme, l'extraction de l'archive peut avoir changé les permissions sur les sous répertoires. Rétablissez-les avec la commande suivante: `find . -type d -exec chmod 0755 {} \;` depuis la racine de votre installation Kohana. + +Vous devriez alors voir la page d'installation contenant un rapport d'installation. Si une erreur est affichée, vous devez la corriger pour pouvoir continuer. + +![Install Page](img/install.png "Example of install page") + +Une fois que votre rapport d'installation vous informe que votre environnement est correctement configuré, vous devez soit renommer, soit supprimer le fichier `install.php`. Vous devriez alors voir apparaitre la page de bienvenue de Kohana: + +![Welcome Page](img/welcome.png "Example of welcome page") diff --git a/includes/kohana/modules/userguide/guide/fr-fr/about.kohana.md b/includes/kohana/modules/userguide/guide/fr-fr/about.kohana.md new file mode 100644 index 00000000..2897852b --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/about.kohana.md @@ -0,0 +1,11 @@ +# Kohana... Kesako? + +Kohana est un [framework web](http://wikipedia.org/wiki/Web_Framework) [PHP5](http://php.net/manual/intro-whatis "PHP Hypertext Preprocessor") open source, [orienté objet](http://wikipedia.org/wiki/Object-Oriented_Programming), adoptant le design pattern [MVC](http://wikipedia.org/wiki/Model–View–Controller "Model View Controller"). Il vise à être rapide, sécurisé et léger. + +[!!] Kohana est licencié sous [licence BSD](http://kohanaphp.com/license), donc vous pouvez légalement l'utiliser pour tout projet open source, commercial ou personnel. + +## Pourquoi Kohana est-il différent? + +Tout peut être surchargé et étendu grâce à son [arborescence de fichiers en cascade](about.filesystem), il y a très peu de [configuration](about.configuration) nécessaire, la [gestion des erreurs](debugging.errors) aide à la localisation rapide de la source des erreurs, et enfin le [debugging](debugging) et le [profiling](debugging.profiling) vous fournissent les statistiques et informations nécessaires sur votre application. + +Pour vous aider dans la sécurisation de votre application, Kohana fournit des protections contre les attaques [XSS](security.xss), des méthodes de [filtrage et de validation des données d'entrées](security.validation), de [signature des cookies](security.cookies), de sécurisation de vos [formulaires](security.forms) et de génération [HTML](security.html). La couche [base de données](security.database) fournit des protections contre l'[injection SQL](http://wikipedia.org/wiki/SQL_Injection). Et bien évidemment, le code officiel est écrit et revu consciencieusement. diff --git a/includes/kohana/modules/userguide/guide/fr-fr/about.mvc.md b/includes/kohana/modules/userguide/guide/fr-fr/about.mvc.md new file mode 100644 index 00000000..16df839f --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/about.mvc.md @@ -0,0 +1,5 @@ +# Modèle Vue Controleur + +Modèle Vue Controleur (ou MVC) est un design pattern populaire visant à séparer les sources de données (Modèle) de la présentation (Vue) et de l'enchainement logique de traitement de la requête (Controleur). + +Il rend plus facile le développement d'application modulaires et réutilisables. \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/fr-fr/about.upgrading.md b/includes/kohana/modules/userguide/guide/fr-fr/about.upgrading.md new file mode 100644 index 00000000..4f0ec30b --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/about.upgrading.md @@ -0,0 +1,290 @@ +# Mise à jour depuis 2.x + +Kohana v3 fonctionne très différemment de Kohana 2.x, néanmoins vous trouverez ci-dessous une liste d'astuces qui pourront vous aider dans votre tâche de mise à jour. + +## Conventions de nommage + +La série 2.x différentie les 'types' de classes (i.e. controleur, modele etc.) en utilisant des suffixes. Dans la série 3.0, cette approche a été abandonnée au profit des conventions du framework Zend c'est-à-dire que les noms de classe sont les chemins vers les classes elles-mêmes. Les répertoires du chemin sont séparés par le caractère underscore au lieu du slashe (i.e. `/some/class/file.php` devient `Some_Class_File`). + +Pour plus d'informations consultez la documentatation sur les [conventions de nommage](start.conventions). + +## Librairie Input + +La librairie Input a été supprimée en faveur de l'utilisation directe des variables `$_GET` et `$_POST`. + +### Protection XSS + +Si vous avez besoin de nettoyer des données contre des attaques XSS, vous pouvez utiliser [Security::xss_clean] de la manière suivante: + + $_POST['description'] = security::xss_clean($_POST['description']); + +Vous pouvez aussi utiliser [Security::xss_clean] en tant que filtre via la librairie [Validate]: + + $validation = new Validate($_POST); + + $validate->filter('description', 'Security::xss_clean'); + +### POST & GET + +Une des fonctionnalités très intéressante de la librairie Input était que lors de la tentative de lecture d'une variable superglobale, si celle-ci n'existait pas, il était possible de spécifier la valeur par défaut retournée i.e.: + + $_GET = array(); + + // On assigne à $id la valeur 1 + $id = Input::instance()->get('id', 1); + + $_GET['id'] = 25; + + // On assigne à $id la valeur 25 + $id = Input::instance()->get('id', 1); + +En 3.0 cette fonctionnalité est rendue par la méthode [Arr::get]: + + $_GET = array(); + + // On assigne à $id la valeur 1 + $id = Arr::get($_GET, 'id', 1); + + $_GET['id'] = 42; + + // On assigne à $id la valeur 42 + $id = Arr::get($_GET, 'id', 1); + +## Librairie ORM + +De nombreux changements majeurs ont été faits sur la librairie ORM depuis la série 2.x, et voici quelques-uns des problèmes les plus courants que vous pourrez rencontrer: + +### Variables de classe + +Toutes les variables de classe sont désormais préfixées par un underscore (_) et ne sont plus accessibles via `__get()`. A la place, vous devez appeler une méthode portant le nom de la propriété sans le caractère underscore. + +Par exemple, la propriété `loaded` en 2.x devient désormais `_loaded` et est accessible depuis l'extérieur via `$model->loaded()`. + +### Relations + +En 2.x, l'itération sur les objets liés à un modèle se faisait comme suit: + + foreach($model->{relation_name} as $relation) + +Cependant avec la nouvelle librarie 3.0 cela ne fonctionnera pas. En effet en version 2.3, toutes les requêtes sont générées avec une portée globale, c'est-à-dire qu'il est impossible de construire 2 requêtes simultanément. Par exemple: + +# TODO: NEED A DECENT EXAMPLE!!!! + +La requête échouera car la seconde requête hérite des conditions de la première et fausse donc les filtres. + +En 3.0 ce problème a été corrigé car chaque requête à sa propre portée. Cela signifie aussi que certains de vos anciens codes ne fonctionneront plus. Prenez par exemple: + + foreach(ORM::factory('user', 3)->where('post_date', '>', time() - (3600 * 24))->posts as $post) + { + echo $post->title; + } + +[!!] (Voir [le tutorial sur la Base de Données](tutorials.databases) pour la nouvelle syntaxe des requêtes) + +En 2.3 on reçoit un itérateur sur tous les posts de l'utilisateur d'id 3 et dont la date est dans l'intervalle spécifié. Au lieu de ça, la condition 'where' sera appliquée au modèle 'user' et la requête retournera un objet `Model_Post` avec les conditions de jointure comme spécifié. + +Pour obtenir le même résultat qu'en 2.x, en 3.0 la structure de la requête doit être modifiée: + + foreach(ORM::factory('user', 3)->posts->where('post_date', '>', time() - (36000 * 24))->find_all() as $post) + { + echo $post->title; + } + +Cela s'applique aussi aux relations `has_one`: + + // Incorrect + $user = ORM::factory('post', 42)->author; + // Correct + $user = ORM::factory('post', 42)->author->find(); + +### Relations Has and belongs to many + +En 2.x vous pouvez spécifier des relations `has_and_belongs_to_many`. En 3.0 cette fonctionnalité a été renommée en `has_many` *through*. + +Dans vos modèles vous définissez une relation `has_many` avec les autres modèles et vous ajoutez un attribut `'through' => 'table'`, où `'table'` est le nom de la table de jointure. Par exemple dans la relation posts<>catégories: + + $_has_many = array + ( + 'categories' => array + ( + 'model' => 'category', // Le modèle étranger + 'through' => 'post_categories' // La table de jointure + ), + ); + +Si vous avez configuré Kohana pour utiliser une prefixe de table vous n'avez pas besoin d'explicitement préfixer la table. + +### Clés étrangères + +En 2.x, pour surcharger une clé étrangère vous deviez spécifier la relation auquelle elle appartenait et ajouter votre nouvelle clé étrangère dans la propriété `$foreign_keys`. + +En 3.0 il faut juste définir une clé `foreign_key` dans la définition de la relation comme suit: + + Class Model_Post extends ORM + { + $_belongs_to = array + ( + 'author' => array + ( + 'model' => 'user', + 'foreign_key' => 'user_id', + ), + ); + } + +Dans cet exemple on doit aussi avoir un champ `user_id` dans la table 'posts'. + + + +Dans les relations has_many le champ `far_key` est le champ de la table de jointure qui le lie à la table étrangère et la clé étrangère est le champ de la table de jointure qui lie la table du modèle courant ("this") avec la table de jointure. + +Considérez la configuration suivante où les "Posts" appartiennent à plusieurs "Categories" via `posts_sections`. + +| categories | posts_sections | posts | +|------------|------------------|---------| +| id | section_id | id | +| name | post_id | title | +| | | content | + + Class Model_Post extends ORM + { + protected $_has_many = array( + 'sections' => array( + 'model' => 'category', + 'through' => 'posts_sections', + 'far_key' => 'section_id', + ), + ); + } + + Class Model_Category extends ORM + { + protected $_has_many = array ( + 'posts' => array( + 'model' => 'post', + 'through' => 'posts_sections', + 'foreign_key' => 'section_id', + ), + ); + } + + +Bien sûr l'exemple d'aliasing présenté ci-dessus est un peu exagéré, mais c'est un bon exemple de fonctionnement des clés foreign/far. + +### Itérateur ORM + +Il est important aussi de noter que `ORM_Iterator` a été renommé en `Database_Result`. + +Si vous avez besoin de récupérer un tableau d'objets ORM dont la clé est la clé étrangère de l'objet, vous devez utiliser [Database_Result::as_array], e.g. + + $objects = ORM::factory('user')->find_all()->as_array('id'); + +où `id` est la clé primaire de la table user. + +## Librairie Router + +En version 2.x il existe une librairie Router qui se charge du traitement des requêtes. Cela permet de définir des routes basiques dans le fichier`config/routes.php` et d'utiliser des expressions régulières mais au détriment de la flexibilité. + +## Routes + +Le sytème de routage est plus flexible en 3.0. Les routes sont maintenant définies dans le fichier bootstrap (`application/bootstrap.php`) et dans le cas des modules dans init.php (`modules/module_name/init.php`). Les routes sont évaluées dans l'ordre dans lequel elles sont définies. + +Aulieu de définir un tableau de routes, désormais on crée un objet [Route] pour chacunes des routes. Contraitement à la version 2.x, il n'est pas nécessaire d'associer une URI à une autre. Au lieu de ça, il faut spécifier un pattern pour une URI en utilisation des variables pour marquer les segments (i.e. controller, method, id). + +Par exemple, en 2.x on créé une route sous forme d'expression régulière comme suit: + + $config['([a-z]+)/?(\d+)/?([a-z]*)'] = '$1/$3/$1'; + +Cette route associe l'URI `controller/id/method` à `controller/method/id`. + +En 3.0 on utilise: + + Route::set('reversed','((/(/)))') + ->defaults(array('controller' => 'posts', 'action' => 'index')); + +[!!] Chaque URI doit avoir un nom unique (dans l'exemple ci-dessus c'est `reversed`). La raison de ce choix est expliquée dans le [tutorial sur les URLs](tutorials.urls). + +Les chevrons sont utilisés pour définir des sections dynamiques qui doivent être transformées en variables. Les parenthèses dénotent une section optionnelle. Si vous ne souhaitez matcher que les URIs commençant par admin, vous pouvez utiliser: + + Rouse::set('admin', 'admin(/(/(/)))'); + +Et si vous voulez forcer l'utilisateur à spécifier un controleur: + + Route::set('admin', 'admin/(/(/))'); + +De plus Kohana 3.0 ne définit pas de routes par défaut. Si votre action (méthode) par défaut est 'index', alors vous devez le spécifier comme tel. Cela se fait via la méthode [Route::defaults]. Si vous voulez utiliser des expressions régulières pour des segments de votre URI alors il suffit de passer un tableau associatif `segment => regex` i.e.: + + Route::set('reversed', '((/(/)))', array('id' => '[a-z_]+')) + ->defaults(array('controller' => 'posts', 'action' => 'index')) + +Cette route force la valeur de `id` à être en minuscule et composée uniquement de caractères alphabétiques et du caractère underscore. + +### Actions + +Une dernière chose importante à noter est que toute méthode accessible d'un controleur (càd via l'URI) sont appelées "actions", et sont préfixées de 'action_'. Dans l'exemple ci-dessus, `admin/posts/1/edit` appelle l'action `edit` mais la méthode rééllement apelée dans le controleur est `action_edit`. Pour plus d'informations voir [le tutorial sur les URLs](tutorials.urls). + +## Sessions + +Les méthodes Session::set_flash(), Session::keep_flash() et Session::expire_flash() n'existent plus. A la place la méthode [Session::get_once] peut être utilisée. + +## Helper URL + +Seules des modifications mineures ont été apportées sur l'helper URL. `url::redirect()` est désormais fait via `$this->request->redirect()` dans les controleurs et via `Request::instance()->redirect()` ailleurs. + +`url::current` a été remplacé par `$this->request->uri()`. + +## Validation + +La syntaxe a subit quelque modifications. Pour valider un tableau il faut maintenant faire: + + $validate = new Validate($_POST); + + // Apply a filter to all items in the arrays + $validate->filter(TRUE, 'trim'); + + // To specify rules individually use rule() + $validate + ->rule('field', 'not_empty') + ->rule('field', 'matches', array('another_field')); + + // To set multiple rules for a field use rules(), passing an array of rules => params as the second argument + $validate->rules('field', array( + 'not_empty' => NULL, + 'matches' => array('another_field') + )); + +La règle 'required' a été renommée en 'not_empty' pour plus de clarté. + +## Librairie View + +En 2.x, les vues sont rendues dans la portée d'un controleur, vous permettant ainsi d'utiliser `$this` dans la vue comme référence vers le controleur. +En 3.0 les vues sont rendues sans aucune portée. Si vous souhaitez utiliser `$this` dans vos vues alors vous devez l'affecter par référence avec [View::bind]: + + $view->bind('this', $this) + +Néanmoins c'est une mauvaise pratique car cela couple votre vue avec le controleur limitant ainsi la réutilisation du code. Il est vivement recommandé de ne passer que les variables requises par la vue: + + $view = View::factory('my/view'); + + $view->variable = $this->property; + + // ou par chainage + + $view + ->set('variable', $this->property) + ->set('another_variable', 42); + + // NON Recommandé + $view->bind('this', $this); + +Etant donné qu'une vue n'a pas de portée, la méthode `Controller::_kohana_load_view` est redondante. Si vous avez besoin de modifier la vue avant qu'elle ne soit rendue (par exemple pour ajouter un menu global à toutes vos pages) vous pouvez utiliser [Controller::after]. + + Class Controller_Hello extends Controller_Template + { + function after() + { + $this->template->menu = '...'; + + return parent::after(); + } + } diff --git a/includes/kohana/modules/userguide/guide/fr-fr/debugging.code.md b/includes/kohana/modules/userguide/guide/fr-fr/debugging.code.md new file mode 100644 index 00000000..7b7b92cd --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/debugging.code.md @@ -0,0 +1,24 @@ +# Debugging du code + +Kohana fournit plusieurs outils puissants pour debugger vos applications. + +Le plus basique d'entre eux est [Kohana::debug]. Cette méthode permet d'afficher toutes variables à la manière de [var_export] ou [print_r], mais en utilisant HTML pour ajouter du formatage supplémentaire. + +~~~ +// Affiche le contenu (dump) des variables $foo et $bar +echo Kohana::debug($foo, $bar); +~~~ + +Kohana fournit aussi une méthode pour afficher le code source d'un fichier en particulier en appelant [Kohana::debug_source]. + +~~~ +// Affiche cette ligne de code source +echo Kohana::debug_source(__FILE__, __LINE__); +~~~ + +Enfin si vous voulez afficher des informations sur les chemins de votre application sans afficher/exposer le chemin d'installation vous pouvez utiliser [Kohana::debug_path]: + +~~~ +// Affiche "APPPATH/cache" plutot que le chemin réél +echo Kohana::debug_file(APPPATH.'cache'); +~~~ diff --git a/includes/kohana/modules/userguide/guide/fr-fr/debugging.errors.md b/includes/kohana/modules/userguide/guide/fr-fr/debugging.errors.md new file mode 100644 index 00000000..b4f9b508 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/debugging.errors.md @@ -0,0 +1,24 @@ +# Gestion des Erreurs/Exceptions + +Kohana fournit des mécanismes de gestion des exceptions et d'erreurs qui transforment les erreurs en exceptions en utilisant les classes PHP prévues à cet effet [ErrorException](http://php.net/errorexception). De nombreux détails sur l'application ainsi que son état sont affichées : + +1. Classe de l'Exception +2. Niveau de l'erreur +3. Message de l'erreur +4. Source de l'erreur, avec la ligne contenant l'erreur surlignée +5. Une [trace de debug](http://php.net/debug_backtrace) du processus d'exécution +6. Les fichiers inclus, les extensions chargées et les variables globales + +## Exemple + +Cliquez sur l'un des liens ci-dessous pour afficher/masquer des informations additionnelles: + +
{{userguide/examples/error}}
+ +## Désactiver le support des Exceptions + +Si vous ne voulez pas utiliser le mécanisme interne de gestion des exceptions et des erreurs, vous pouvez le désactiver via [Kohana::init]: + +~~~ +Kohana::init(array('errors' => FALSE)); +~~~ \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/fr-fr/debugging.profiling.md b/includes/kohana/modules/userguide/guide/fr-fr/debugging.profiling.md new file mode 100644 index 00000000..4dea4b6e --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/debugging.profiling.md @@ -0,0 +1,22 @@ +# Profiling + +Kohana fournit de façon très facile les statistiques de vos applications: + +1. Appels de méthodes [Kohana] communes +2. Requêtes URI +3. Requêtes de [base de données](tutorials.databases) +4. Temps moyen d'execution de votre application + +## Affichage/Récupération des statistiques + +Vous pouvez afficher ou récupérer les statistiques courantes à tout moment en faisant: + +~~~ +
+ +
+~~~ + +## Exemple + +{{profiler/stats}} \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/fr-fr/features.md b/includes/kohana/modules/userguide/guide/fr-fr/features.md new file mode 100644 index 00000000..068341da --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/features.md @@ -0,0 +1 @@ +Cette page liste les fonctionnalités clés de Kohana v3. \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/fr-fr/menu.md b/includes/kohana/modules/userguide/guide/fr-fr/menu.md new file mode 100644 index 00000000..2c8bca8a --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/menu.md @@ -0,0 +1,26 @@ +1. **Bien débuter** + - [Qu'est-ce que Kohana?](about.kohana) + - [Conventions et style](about.conventions) + - [Installation](about.install) + - [Mise à jour depuis 2.x](about.upgrading) + - [Configuration](about.configuration) + - [Modèle Vue Controleur](about.mvc) + - [Arborescence de fichier](about.filesystem) + - [Auto-chargement](about.autoloading) + - [Enchainement des Requetes](about.flow) + - [Explorateur API](api) +2. **Tutoriaux** + - [Hello, World](tutorials.helloworld) + - [Routes, URLs, et Liens](tutorials.urls) + - [Base de données](tutorials.databases) + - [ORM](tutorials.orm) + - [Travailler avec Git](tutorials.git) +3. **Securité** + - [XSS](security.xss) + - [Validation](security.validation) + - [Cookies](security.cookies) + - [Base de données](security.database) +4. **Debugging** + - [Code](debugging.code) + - [Gestion des erreurs](debugging.errors) + - [Profiling](debugging.profiling) diff --git a/includes/kohana/modules/userguide/guide/fr-fr/security.cookies.md b/includes/kohana/modules/userguide/guide/fr-fr/security.cookies.md new file mode 100644 index 00000000..cd999df1 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/security.cookies.md @@ -0,0 +1,3 @@ +# Sécurité des Cookies + +[!!] stub diff --git a/includes/kohana/modules/userguide/guide/fr-fr/security.database.md b/includes/kohana/modules/userguide/guide/fr-fr/security.database.md new file mode 100644 index 00000000..9046f266 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/security.database.md @@ -0,0 +1,3 @@ +# Sécurité de la Base de données + +[!!] stub diff --git a/includes/kohana/modules/userguide/guide/fr-fr/security.validation.md b/includes/kohana/modules/userguide/guide/fr-fr/security.validation.md new file mode 100644 index 00000000..9a5b3f97 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/security.validation.md @@ -0,0 +1,241 @@ +# Validation + +La validation peut être effectuée sur tous les tableaux en utilisant la classe [Validate]. Les labels, filtres, règles et callbacks peuvent être attachés à un objet Validate via un tableau de clé, appellées "champs" (field name). + +labels +: Un label est la version lisible (par un humain) d'un nom de champ. + +filters +: Un filtre modifie la valeur d'un champs avant que les règles et callbacks ne soient exécutées. + +rules +: Une règle est une vérification sur un champ qui retourne `TRUE` ou `FALSE`. Si une règle retourne `FALSE`, une erreur sera ajoutée à ce champ. + +callbacks +: Une callback est une méthode spécifique ayant accès à l'ensemble de l'objet Validate. + La valeur retournée par une callback est ignorée. A la place, en cas d'erreur une callback doit manuellement ajouter une erreur à un champ en utilisant [Validate::error]. + +[!!] A noter que les callbacks [Validate] et les [callbacks PHP](http://php.net/manual/language.pseudo-types.php#language.types.callback) ne sont pas pareils. + +Utiliser `TRUE` comment nom de champ lors de l'ajout d'un filtre, règle ou callback a pour effet de l'appliquer à tous les champs. + +**L'objet [Validate] supprimera tous les champs du tableau qui n'ont pas explicitement été utilisés via un label, un filtre, une règle ou une callback. Ceci pour empêcher tout accès à un champ qui n'a pas été validé et ajouter ainsi une protection de sécurité supplémentaire.** + +La création d'un objet de validation est faite en utilsiant la méthode [Validate::factory]: + + $post = Validate::factory($_POST); + +[!!] L'objet `$post` sera utilisé pour le reste de ce tutorial dans lequel sera illustré la validation de l'inscription d'un nouveal utilisateur. + +### Règles par défaut + +La validation supporte les règles par défaut suivantes: + +Nom de la règle | Description +------------------------- |------------------------------------------------- +[Validate::not_empty] | La valeur ne doit pas être vide +[Validate::regex] | La valeur respecte l'expression réguliére spécifiée +[Validate::min_length] | La valeur respecte un nombre minimum de caractères +[Validate::max_length] | La valeur respecte un nombre maximum de caractères +[Validate::exact_length] | La valeur fait exactement le nombre de caractéres spécifiés +[Validate::email] | La valeur doit respecter un format d'email +[Validate::email_domain] | Le domaine de l'email existe +[Validate::url] | La valeur entrée doit respecter un format d'URL +[Validate::ip] | La valeur entrée doit respecter un format d'adresse IP +[Validate::phone] | La valeur entrée doit respecter un format d'un uméro de téléphone +[Validate::credit_card] | La valeur entrée doit respecter un format de numéro de carte de crédit +[Validate::date] | La valeur entrée doit respecter un format de date (et heure) +[Validate::alpha] | Seuls les caractères alphabétiques sont autorisés +[Validate::alpha_dash] | Seuls les caractères alphabétiques et le caractère tiret '-' sont autorisés +[Validate::alpha_numeric] | Seuls les caractères alphabétiques et numériques sont autorisés +[Validate::digit] | La valeur doit être un chiffre +[Validate::decimal] | La valeur doit être décimale ou flottante +[Validate::numeric] | Seuls les caractères numériques sont autorisés +[Validate::range] | La valeur doit être dans l'intervalle spécifié +[Validate::color] | La valeur entrée doit respecter un format de couleur hexadécimal +[Validate::matches] | La valeur doit correspondre à la valeur d'un autre champ + +[!!] Toute méthode existante de la classe [Validate] peut être utilisée directement sans utiliser une déclaration de callback compléte. Par exemple, ajouter la règle `'not_empty'` est la même chose que `array('Validate', 'not_empty')`. + +## Ajouter des filtres + +Tous les filtres de validation sont définis par un nom de champ, une méthode ou une fonction (en utilisant la syntaxe des [callbacks PHP](http://php.net/manual/language.pseudo-types.php#language.types.callback)), ainsi q'un tableau de paramètres: + + $object->filter($field, $callback, $parameter); + +Les filtres modifient la valeur d'un filtre avant leur vérification par les règles et callbacks définies. + +Par exemple pour convertir un nom d'utilisateur en minuscule, alors il suffit d'écrire: + + $post->filter('username', 'strtolower'); + +Autre exemple, si l'on souhaite enlever les caratères vides au début et en fin de chaine de tous les champs, alors il faut écrire: + + $post->filter(TRUE, 'trim'); + +## Ajouter des règles + +Toutes les règles de validation sont définies par un nom de champ, une méthode ou une fonction (en utilisant la syntaxe des [callbacks PHP](http://php.net/manual/language.pseudo-types.php#language.types.callback)), ainsi q'un tableau de paramètres: + + $object->rule($field, $callback, $parameter); + +Pour commencer notre exemple, nous allons commencer par valider le tableau `$_POST` contenant des informations d'inscription d'un utilisateur. + +Pour cela nous avons besoin de traiter les informations POSTées en utilisant [Validate]. Commencons par ajouter quelque régles: + + $post + ->rule('username', 'not_empty') + ->rule('username', 'regex', array('/^[a-z_.]++$/iD')) + + ->rule('password', 'not_empty') + ->rule('password', 'min_length', array('6')) + ->rule('confirm', 'matches', array('password')) + + ->rule('use_ssl', 'not_empty'); + +Toute fonction PHP existante peut aussi être utilisée comme une règle. Par exemple si l'on souhaite vérifier que l'utilisateur a entré une valeur correcte pour une question, on peut écrire: + + $post->rule('use_ssl', 'in_array', array(array('yes', 'no'))); + +A noter que les tableaux de paramètres doivent quand même être insérés dans un tableau! Si vous ne mettez pas ce tableau, `in_array` serait appelée via `in_array($value, 'yes', 'no')`, ce qui aboutirait à une erreur PHP. + +Toute régle spécifique peut être ajoutée en utilisant une [callback PHP](http://php.net/manual/language.pseudo-types.php#language.types.callback): + + $post->rule('username', array($model, 'unique_username')); + +La méthode `$model->unique_username()` ressemblerait alors à: + + public function unique_username($username) + { + // Vérifie si le nom d'utilisateur existe déjà dans la base de données + return ! DB::select(array(DB::expr('COUNT(username)'), 'total')) + ->from('users') + ->where('username', '=', $username) + ->execute() + ->get('total'); + } + +[!!] Vous pouvez définir vos propres régles pour faire des vérifications additionnelles. Ces régles peuvent être réutilisés à plusieurs fins. Ces méthodes vont presque toujours exister au sein d'un modèle mais peuvent être définies dans nimporte quelle classe. + +## Ajouter des callbacks + +Toutes les callbacks de validation sont définies par un nom de champ et une méthode ou une fonction (en utilisant la syntaxe des [callbacks PHP](http://php.net/manual/language.pseudo-types.php#language.types.callback)): + + $object->callback($field, $callback); + +[!!] Contrairement aux filtres et aux régles, aucun paramètre n'est passé à une callback. + +Le mot de passe utilisateur doit être hashé parès validaiton, nous allons donc le faire avec une callback: + + $post->callback('password', array($model, 'hash_password')); + +Cela implique la création de la méthode `$model->hash_password()` de la manière suivante: + + public function hash_password(Validate $array, $field) + { + if ($array[$field]) + { + // Hasher le mot de passe s'il existe + $array[$field] = sha1($array[$field]); + } + } + +# Un exemple complet + +TOut d'abord nous avons besoin d'une [Vue] contenant le formulaire HTML que l'on placera dans `application/views/user/register.php`: + + + +

Des erreurs ont été trouvées, veuillez vérifier les informations entrées.

+
    + +
  • + + + +
    +
    +
    + +
    +
    +
    Le mot de passe doit contenir au moins 6 caractères.
    +
    +
    + +
    +
    'Toujours', 'no' => 'Seulement si nécessaire'), $post['use_ssl']) ?>
    +
    Pour des raisons de sécurité, SSL est toujours utilisé pour les paiements.
    +
    + + + + +[!!] Cette exemple utilise le helper [Form]. L'utiliser au lieu d'écrire du code HTML vous assure que tous les objets du formulaire vont traiter correctement les caractères HTML. Si vous souhaitez écrire le code HTML directement, veillez à utiliser [HTML::chars] pour filtrer/échaper les informations entrées par les utilisateurs. + +Ensuite nous avons besoin d'un controleur et d'une action pour traiter l'inscription des utilisaeurs, fichier qu'on placera dans `application/classes/controller/user.php`: + + class Controller_User extends Controller { + + public function action_register() + { + $user = Model::factory('user'); + + $post = Validate::factory($_POST) + ->filter(TRUE, 'trim') + + ->filter('username', 'strtolower') + + ->rule('username', 'not_empty') + ->rule('username', 'regex', array('/^[a-z_.]++$/iD')) + ->rule('username', array($user, 'unique_username')) + + ->rule('password', 'not_empty') + ->rule('password', 'min_length', array('6')) + ->rule('confirm', 'matches', array('password')) + + ->rule('use_ssl', 'not_empty') + ->rule('use_ssl', 'in_array', array(array('yes', 'no'))) + + ->callback('password', array($user, 'hash_password')); + + if ($post->check()) + { + // Les données ont éta validées, on inscrit l'utilisateur + $user->register($post); + + // Toujours rediriger l'utilisateur après une validation de formulaire réussie afin de ne pas avoir les avertissement de rafraichissement + $this->request->redirect('user/profile'); + } + + // La validation a échoué, récupérons les erreurs + $errors = $post->errors('user'); + + // Affiche le formulaire d'inscription + $this->request->response = View::factory('user/register') + ->bind('post', $post) + ->bind('errors', $errors); + } + + } + +Nous avons aussi besoin d'un modèle qui sera placé dans `application/classes/model/user.php`: + + class Model_User extends Model { + + public function register($array) + { + // Créé un nouvel utilisateur dans la base de données + $id = DB::insert(array_keys($array)) + ->values($array) + ->execute(); + + // Sauvegarde l'identifiant de l'utilisateur dans un cookie + cookie::set('user', $id); + + return $id; + } + + } + +C'est tout! Nous avons désormais un formulaire d'inscritpion opérationnel et qui vérifie les informations entrées. \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/fr-fr/security.xss.md b/includes/kohana/modules/userguide/guide/fr-fr/security.xss.md new file mode 100644 index 00000000..374f8fbe --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/security.xss.md @@ -0,0 +1,17 @@ +# Cross-Site Scripting (XSS) + +La première étape pour se prémunir des attaques de type [XSS](http://wikipedia.org/wiki/Cross-Site_Scripting) est de savoir quand il faut le faire. Les attaques XSS ne peuvent être déclenchées que lors de l'affichage de contenu HTML au travers de formulaires ou de données issues de la base de données. Toute variable globale contenant des informations clientes peut être un vecteur d'attaques XSS. Cela inclut les données `$_GET`, `$_POST`, et `$_COOKIE`. + +## Prévention + +Il existe des règles simples à suivre pour prémunir vos applications de ces attaques. + +La première est d'utiliser systématiquement la méthode [Security::xss] pour nettoyer des données d'une variable globale. De plus si vous ne souhaitez pas avoir de HTML dans vos variables, utilisez la méthode [strip_tags](http://php.net/strip_tags) pour supprimer les balises HTML. + +[!!] Si vous autorisez les utilisateurs à entrer des données HTML dans votre application, il est vivement recommandé d'utiliser une librairie de nettoyage HTML comme [HTML Purifier](http://htmlpurifier.org/) ou [HTML Tidy](http://php.net/tidy). + +La seconde est de toujours échapper les données insérées dans vos pages HTML. La classe [HTML] fournit des générateurs pour de nombreuses balises HTML, incluant scripts et feuilles de style, liens, ancres, images et email. Tout contenu sans confiance doit être échappé avec [HTML::chars]. + +## Références + +* [OWASP XSS Cheat Sheet](http://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet) diff --git a/includes/kohana/modules/userguide/guide/fr-fr/tutorials.databases.md b/includes/kohana/modules/userguide/guide/fr-fr/tutorials.databases.md new file mode 100644 index 00000000..f21aefe9 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/tutorials.databases.md @@ -0,0 +1,242 @@ +# Base de données {#top} + +Kohana 3.0 intégre un robuste module permettant de travailler avec les base de données. Par défauut, le module supporte [MySQL](http://php.net/mysql) et [PDO](http://php.net/pdo). + +Le module base de données est inclus par défaut dans votre installation de Kohana 3.0 mais n'est pas activé. Pour l'activer, éditez le fichier `application/bootstrap.php` et modifiez l'appel à [Kohana::modules] pour y inclure le module base de données: + + Kohana::modules(array( + ... + 'database' => MODPATH.'database', + ... + )); + +## Configuration {#configuration} + +Aprés activation du module, il vous faut préciser les paramètres de configuration permettant à votre application de se connecter à la base de données. Un exemple de fichier de configuration peut être trouvé sous `modules/database/config/database.php`. + +La structure d'un groupe de configuration pour une base de données, appelé instance, est de cette forme: + + string INSTANCE_NAME => array( + 'type' => string DATABASE_TYPE, + 'connection' => array CONNECTION_ARRAY, + 'table_prefix' => string TABLE_PREFIX, + 'charset' => string CHARACTER_SET, + 'profiling' => boolean QUERY_PROFILING, + ), + +[!!] Plusieurs instances différentes de ces configurations peuvent être définies dans le fichier de configuration. + +La compréhension de l'ensemble de ces paramètres est importante: + +INSTANCE_NAME +: nom personnalisé de l'instance. Il est obligatoire d'avoir au moins une instance appelée "default". + +DATABASE_TYPE +: type de base de données. Valeurs acceptées: "mysql" et "pdo". + +CONNECTION_ARRAY +: options de connection spécifiques au type de base de données choisis. Ces options sont explicités [plus bas](#connection_settings). + +TABLE_PREFIX +: prefixe qui sera ajouté à tous les noms de table par le [constructeur de requêtes](#query_building). + +QUERY_PROFILING +: activer le [profiling](debugging.profiling) des requêtes. + +### Exemple + +L'exemple ci-dessous est composé de 2 connections MySQL, la première locale et l'autre distante: + + return array + ( + 'default' => array + ( + 'type' => 'mysql', + 'connection' => array( + 'hostname' => 'localhost', + 'username' => 'dbuser', + 'password' => 'mypassword', + 'persistent' => FALSE, + 'database' => 'my_db_name', + ), + 'table_prefix' => '', + 'charset' => 'utf8', + 'profiling' => TRUE, + ), + 'remote' => array( + 'type' => 'mysql', + 'connection' => array( + 'hostname' => '55.55.55.55', + 'username' => 'remote_user', + 'password' => 'mypassword', + 'persistent' => FALSE, + 'database' => 'my_remote_db_name', + ), + 'table_prefix' => '', + 'charset' => 'utf8', + 'profiling' => TRUE, + ), + ); + +### Options de connection {#connection_settings} + +Chacun des types de base de données possède des options différentes de connection. + +#### MySQL + +Les options de connection MySQL sont les suivantes: + +Type | Option | Description | Valeur par défaut +----------|------------|----------------------------|-------------------------- +`string` | hostname | Hôte hébergeant la base | `localhost` +`integer` | port | Numéro de port | `NULL` +`string` | socket | Socket UNIX | `NULL` +`string` | username | Utilisateur | `NULL` +`string` | password | Mot de passe | `NULL` +`boolean` | persistent | Connections persistantes | `FALSE` +`string` | database | Nom de base de la base | `kohana` + +#### PDO + +Les options de connection PDO sont les suivantes: + +Type | Option | Description | Valeur par défaut +----------|------------|----------------------------|-------------------------- +`string` | dsn | Source PDO | `localhost` +`string` | username | Utilisateur | `NULL` +`string` | password | Mot de passe | `NULL` +`boolean` | persistent | Connections persistantes | `FALSE` + +!! Si vous utilisez PDO et n'êtes pas sûr de la valeur du `dsn`, veuillez consulter [PDO::__construct](http://php.net/pdo.construct). + +## Connections et Instances {#connections} + +Chaque groupe de configuration est accessible en tant qu'instance de base de données. On accède à une instance en appelant [Database::instance]: + + $default = Database::instance(); + $remote = Database::instance('remote'); + +Pour se déconnecter de la base de données, il suffit de détruire l'objet correspondant: + + unset($default, Database::$instances['default']); + +Si vous souhaitez déconnecter l'ensemble des instances d'un coup alors écrivez: + + Database::$instances = array(); + +## Ecrire des requêtes {#making_queries} + +Il existe 2 manières d'écrire des requêtes dans Kohana. La manière la plus simple est d'utiliser le [constructeur de requête](Query_Builder), via [DB::query]. Ces requêtes sont appelées des "requêtes préparées" ou prepared statements et permettent l'échappement automatique des paramètres de la requête. + +La seconde manière est d'appeler directement les méthodes voulues. + +[!!] Toutes les requêtes sont executées via la méthode `execute`, qui prend en paramètre un objet base de données ou un nom d'instance. Pour plus d'informations, consultez [Database_Query::execute]. + +### Requêtes préparées + +L'utilisation de requêtes préparées permet d'écrire des requetes SQL manuellement tout en échappant les paramètres de la requête automatiquement permettant ainsi de se prémunir contre les [injections SQL](http://wikipedia.org/wiki/SQL_Injection). La création d'une requête est simple: + + $query = DB::query(Database::SELECT, 'SELECT * FROM users WHERE username = :user'); + +La méthode [DB::query] créé un objet [Database_Query] et permet un chainage des méthodes. La requête contient un paramètre `:user` que l'on peut assigner comme suit: + + $query->param(':user', 'john'); + +[!!] Les noms de paramètre peuvent être nimporte quelle chaine de caractères puisqu'elles sont remplacées en utilisant la fonction [strtr](http://php.net/strtr). Il est vivement recommandé de ne **pas** utiliser de signe dollars ($) pour éviter toute confusion. + +Si vous souhaitez afficher la requête SQL qui va être exécutée, il vous suffit de caster l'objet en chaine de caractères comme suit: + + echo Kohana::debug((string) $query); + // Affichera: + // SELECT * FROM users WHERE username = 'john' + +Vous pouvez aussi ré-assigner `:user` ultérieurement en appelant [Database_Query::param]: + + $query->param(':user', $_GET['search']); + +[!!] Pour assigner plusieurs paramètres à la fois, vous pouvez utiliser [Database_Query::parameters]. + +Une fois chacuns des paramètres de votre requête assignés, l'exécution de la requête se fait via: + + $query->execute(); + +Enfin, il est aussi possible d'assigner un paramètre à une [variable passée par référence](http://php.net/language.references.whatdo). Cela peut s'avérer très utile lors de l'exécution de la même requête plusieurs fois avec des paramètres différents: + + $query = DB::query(Database::INSERT, 'INSERT INTO users (username, password) VALUES (:user, :pass)') + ->bind(':user', $username) + ->bind(':pass', $password); + + foreach ($new_users as $username => $password) + { + $query->execute(); + } + +Dans l'exemple ci-dessus, les variables `$username` and `$password` sont changées à chacune des itérations de la boucle `foreach`. Cela s'avére très puissant et peut vous permettre d'alléger votre code. + +### Construction de requêtes {#query_building} + +La création dynamique de requêtes en utilisant des objets et des méthodes de classe permet de créér des requêtes sans avoir de connaissances sur le langage SQL. Le constructeur se charge d'échapper les noms de table et colonnes mais aussi les valeurs des paramètres des requêtes. + +[!!] A ce jour, Kohana ne dispose pas de moyens de combiner les requêtes préparées et la construction dynamique de requêtes. + +#### SELECT + +Chaque type de requête en base de données est représenté par une classe, chacunes possédant ses propres méthodes. Par exemple, pour créér une requête SELECT, utilisez [DB::select]: + + $query = DB::select()->from('users')->where('username', '=', 'john'); + +Par défault, [DB::select] sélectionnera toutes les colonnes (`SELECT * ...`), mais vous pouvez aussi spécifier ces colonnes: + + $query = DB::select('username', 'password')->from('users')->where('username', '=', 'john'); + +L'exemple ci-dessus illustre aussi la puissance du chainage de méthodes qui permet en une seule ligne de spécifier les paramètres de sélection, la table et les critères de filtrage via la méthode `where`. De la même manière que précédemment, si vous souhaitez afficher la requête SQL qui va être exécutée, il vous suffit de caster l'objet en chaine de caractères comme suit: + + echo Kohana::debug((string) $query); + // Affichera: + // SELECT `username`, `password` FROM `users` WHERE `username` = 'john' + +Notez que tout est échappé correctement et c'est là l'un des grands avantages de l'utilisation du constructeur de requêtes. + +La création d'alias `AS` se fait comme ci-dessous: + + $query = DB::select(array('username', 'u'), array('password', 'p'))->from('users'); + // Requête exécutée: + // SELECT `username` AS `u`, `password` AS `p` FROM `users` + +#### INSERT + +Pour insérer des enregistrements dans la base de données il faut utiliser [DB::insert]: + + $query = DB::insert('users', array('username', 'password'))->values(array('fred', 'p@5sW0Rd')); + // Requête exécutée: + // INSERT INTO `users` (`username`, `password`) VALUES ('fred', 'p@5sW0Rd') + +#### UPDATE + +La modification d'un enregistrement en base se fait via [DB::update]: + + $query = DB::update('users')->set(array('username' => 'jane'))->where('username', '=', 'john'); + // Requête exécutée: + // UPDATE `users` SET `username` = 'jane' WHERE `username` = 'john' + +#### DELETE + +Pour supprimer un enregistrement, il faut utiliser [DB::delete]: + + $query = DB::delete('users')->where('username', 'IN', array('john', 'jane')); + // Requête exécutée: + // DELETE FROM `users` WHERE `username` IN ('john', 'jane') + +#### Fonctions spécifiques {#database_functions} + +Il est commun d'utiliser des fonctions spécifiques de base de données telles que `COUNT`. Le constructeur de requête vous permet de les utiliser de 2 manières. La première est la suivante: + + $query = DB::select(array('COUNT("username")', 'total_users'))->from('users'); + +Ca ressemble beaucoup à l'aliasing `AS` mais notez que le nom de colonne est entouré de doubles quotes. A chaque fois qu'un nom de colonne est entouré de doubles quotes, alors **seules** les parties entourées seront échappées. Cette requête générerait le SQL suivant: + + SELECT COUNT(`username`) AS `total_users` FROM `users` + +#### Expressions complexes + +De temps à autre on a besoin d'écrire des requêtes contenant des expressions complexes. Dans ce cas, cette expression sera créé via [DB::expr]. Une expression est prise telle quelle par la méthode et de ce fait aucun échappement n'est fait. \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/fr-fr/tutorials.git.md b/includes/kohana/modules/userguide/guide/fr-fr/tutorials.git.md new file mode 100644 index 00000000..e8cac61f --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/tutorials.git.md @@ -0,0 +1,117 @@ +# Travailler avec Git + +Kohana utilise [git](http://git-scm.com/) comme système de gestion de versions et [github](http://github.com/kohana) pour l'aspect collaboratif. Ce tutorial présente comment utiliser git et github pour mettre en place une application simple. + +## Structure initiale + +[!!] Ce tutorial prend comme prérequis le fait que votre serveur web est déjà mis en place et que vous être dans l'étape de création d'une nouvelle application située à . + +En utilisant votre console, placez vous dans le répertoire `gitorial` et exécutez `git init`. Cela créera la structure du dépôt git. + +Ensuite, nous allons créér un [sous-module](http://www.kernel.org/pub/software/scm/git/docs/git-submodule.html) pour le répertoire `system`. Allez à l'URL et copiez l'URL de clonage: + +![Github Clone URL](http://img.skitch.com/20091019-rud5mmqbf776jwua6hx9nm1n.png) + +Maintenant utilisez cette URL pour créér le sous-module `system`: + +~~~ +git submodule add git://github.com/kohana/core.git system +~~~ + +[!!] Cela créera un lien vers la version stable en développement. La version stable en développement est sûre à utiliser pour vos environnements de production et possède la même API que la version stable en téléchargement à laquelle sont ajoutés les correctifs de bugs. + +A partir de là vous pouvez ajouter les modules que vous souhiatez, par exemple le module [Base de données](http://github.com/kohana/database): + +~~~ +git submodule add git://github.com/kohana/database.git modules/database +~~~ + +Une fois les sous-modules ajoutés, vous devez les initialiser: + +~~~ +git submodule init +~~~ + +Enfin il faut les commiter: + +~~~ +git commit -m 'Added initial submodules' +~~~ + +L'étape suivante consiste en la création de la structure des répertoires de votre application kohana. Le minimum requis est: + +~~~ +mkdir -p application/classes/{controller,model} +mkdir -p application/{config,views} +mkdir -m 0777 -p application/{cache,logs} +~~~ + +Si vous lancez la commande linux `find application` vous devez voir: + +~~~ +application +application/cache +application/config +application/classes +application/classes/controller +application/classes/model +application/logs +application/views +~~~ + +Puisque l'on ne souhaite pas que les changements sur les logs et les mises en cache soient pris en compte, il faut ajouter un fichier `.gitignore` à chacun de ces répertoires. Cela aura pour effet d'ignorer tous les fichiers non cachés du répertoire: + +~~~ +echo '[^.]*' > application/{logs,cache}/.gitignore +~~~ + +[!!] Git ignore les répertoires vides, donc le fait d'ajouter le fichier `.gitignore` vous assure que git prendra en compte le répertoire mais pas les fichiers qu'il contient. + +Ensuite il faut récupérer les fichiers `index.php` et `bootstrap.php`: + +~~~ +wget http://github.com/kohana/kohana/raw/master/index.php +wget http://github.com/kohana/kohana/raw/master/application/bootstrap.php -O application/bootstrap.php +~~~ + +Commiter tous les changements: + +~~~ +git add application +git commit -m 'Added initial directory structure' +~~~ + +C'est tout! Vous avez désormais une application gérée sous Git. + +## Mettre à jour les sous-modules + +Tôt ou tard vous allez sûrement avoir besoin de mettre à jour vos sous-modules. Pour mettre à jour l'ensemble de vos sous-modules à la version la plus récente `HEAD`, entrez: + +~~~ +git submodule foreach +~~~ + +Pour mettre à jour un seul sous-module, par exemple `system`, entrez: + +~~~ +cd system +git checkout master +git fetch +git merge origin/master +cd .. +git add system +git commit -m 'Updated system to latest version' +~~~ + +Enfin si vous souhaitez mettre à jour un sous-module par rapport à une révision particulière, entrez: + +~~~ +cd modules/database +git fetch +git checkout fbfdea919028b951c23c3d99d2bc1f5bbeda0c0b +cd ../.. +git add database +git commit -m 'Updated database module' +~~~ + + diff --git a/includes/kohana/modules/userguide/guide/fr-fr/tutorials.helloworld.md b/includes/kohana/modules/userguide/guide/fr-fr/tutorials.helloworld.md new file mode 100644 index 00000000..c4360b39 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/tutorials.helloworld.md @@ -0,0 +1,103 @@ +# Hello, World + +Tout framework digne de ce nom possède un exemple d'application "Hello World", alors ne dérogeons pas à la régle! + +On commencera donc par décrire un "hello word" très très basique puis on détaillera les principes MVC appliqués à l'exemple. + +## Au commencement il n'y avait rien... + +La première chose à faire est de créer un controleur de telle sorte que Kohana puisse traiter une requête. + +Créér le fichier `application/classes/controller/hello.php` dans votre répertoire application et ajoutez-y le code suivant: + + template->message = 'hello, world!'; + } + } + +`extends Controller_Template` +: nous héritons désormais du controleur template qui rend plus facile l'utilisation de vues au sein d'un controleur. + +`public $template = 'site';` +: le controleur template doit connaitre le template que vous souhaitez utiliser. Il chargera alors automatiquement la vue en question et lui assignera l'objet Vue créé. + +`$this->template->message = 'hello, world!';` +: `$this->template` est une référence vers l'objet Vue du template de notre site. Ce que l'on fait ici est assigner à la vue la variable "message" dont la valeur est "hello, world!". + +Maintenant actualisez votre navigateur... + +
    {{userguide/examples/hello_world_error}}
    + +Kohana vous affiche une erreur au lieu du message fascinant qu'il devrait afficher. En regardant de plus près le message d'erreur on peut voir que la librairie View n'a pas été capable de trouver notre template, probablement parceque nous ne l'avons pas encore créé! + +Créons donc notre vue en créant le fichier `application/views/site.php` avec le texte suivant: + + + + We've got a message for you! + + + +

    +

    We just wanted to say it! :)

    + + + +Maintenant si vous ré-actualisez, vous devriez voir apparaitre ce qu'il faut: + +![hello, world! We just wanted to say it!](img/hello_world_2.png "hello, world! We just wanted to say it!") + +## A moi la gloire et l'argent! + +Dans ce tutorial on a abordé comment créer un controleur et utiliser une vue pour séparer la logique de la présentation. + +Evidemment l'exemple choisi est une introduction basique à Kohana et n'effleure même pas les possibilités infinies de Kohana ;). \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/fr-fr/tutorials.orm.md b/includes/kohana/modules/userguide/guide/fr-fr/tutorials.orm.md new file mode 100644 index 00000000..ee7d9bbf --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/tutorials.orm.md @@ -0,0 +1,121 @@ +# ORM {#top} + +Kohana 3.0 inclus un module [ORM](http://en.wikipedia.org/wiki/Object-relational_mapping) puissant utilisant le pattern active record et l'introspection de base de données pour déterminer les informations sur les colonnes d'un modèle. + +Bien que le module ORM soit inclus par défaut dans vos installations de Kohana 3.0, il est désactivé par défaut. Pour l'activer modifiez le fichier `application/bootstrap.php` et ajoutez à l'appel [Kohana::modules] le module ORM: + + Kohana::modules(array( + ... + 'orm' => MODPATH.'orm', + ... + )); + +## Configuration {#configuration} + +Pour pouvoir utiliser l'ORM, il faut tout d'abord faire hériter vos modèles de la classe ORM comme suit: + + class Model_User extends ORM + { + ... + } + +Dans l'exemple ci-dessus, le modele cherchera une table `users` dans la base de données par défaut. + +### Propriétés d'un modèle ORM + +Les propriétés suivantes peuvent être utilisées pour configurer chacuns de vos modèles: + +Type | Option | Description | Valeur par défaut +----------|-----------------|--------------------------------------| ------------------------- +`string` | _table_name | Nom de la table | +`string` | _db | Nom de la base de données |`default` +`string` | _primary_key | Colonne contenant la clé primaire |`id` +`string` | _primary_val | Colonne contenant la valeur primaire |`name` + +## Utiliser l'ORM + +### Charger un enregistrement + +Pour créér une instance d'un modèle, il faut utiliser la méthode [ORM::factory] ou passer directement par le constructeur: + + $user = ORM::factory('user'); + // ou + $user = new Model_User(); + +Les 2 méthodes ci-dessus peuvent prendre en argument la valeur de la clé primaire de l'élément que l'on souhaite charger: + + // Charge l'utilisateur d'ID 5 + $user = ORM::factory('user', 5); + +[ORM::loaded] permet de savoir si un modèle donné a été chargé avec succès. + +### Rechercher un enregistrement + +L'ORM supporte la plupart des méthodes [Base de données](tutorials.databases) pour affiner les recherches sur les données du modèle. Pour avoir une liste complète, référez vous à la propriété `_db_methods`. Les enregistrements sont récupérés lors de l'appel à [ORM::find] ou [ORM::find_all]. + + // Le code ci-dessous récupère le premier utilisateur actif prénommé Bob + $user = ORM::factory('user') + ->where('active', '=', TRUE) + ->where('name', '=', 'Bob') + ->find(); + + // Le code ci-dessous récupère tous les utilisateur actifs Bob + $users = ORM::factory('user') + ... + ->find_all(); + +Lors de la récupération d'une liste de modèles par la méthode [ORM::find_all], le parcours des éléments se fait comme pour les résultats de base de données: + + foreach ($users as $user) + { + ... + } + +### Accès aux propriétés d'un Modèle + +Toutes les propriétés d'un modèle sont accessibles en utilisant les méthodes magiques `__get` et `__set`. + + $user = ORM::factory('user', 5); + + // Affiche le nom de l'utilisateur + echo $user->name; + + // Change le nom de l'utilisateur + $user->name = 'Bob'; + +Pour stocker des informations/propriétés qui n'ont pas de correspondances dans la table (c'est-à-dire aucune colonnes du même nom que la propriété), il faut utiliser la propriété `_ignored_columns`: + + class Model_User extends ORM + { + ... + protected $_ignored_columns = array('field1', 'field2', ...) + ... + } + +### Créér et sauvegarder des enregistrements + +La méthode [ORM::save] est utilisée aussi bien pour créer et sauvegarder de nouveaux enregistrements que pour mettre à jour des enregistrements existants. + + // Création d'un enregistrement + $user = ORM::factory('user'); + $user->name = 'New user'; + $user->save(); + + // Mise à jour d'un enregistrement + $user = ORM::factory('user', 5); + $user->name = 'User 2'; + $user->save(); + +Vous pouvez mettre à jour plusieurs enregistrements à la fois en utilisant la méthode [ORM::save_all]: + + $user = ORM::factory('user'); + $user->name = 'Bob'; + + // Change le nom de tous les utilisateurs actifs à Bob + $user->where('active', '=', TRUE)->save_all(); + +[ORM::saved] permet de vérifier si le modèle a bien été sauvegardé. + +### Supprimer des enregistrements + +La suppression d'enregistrements se fait avec [ORM::delete] et [ORM::delet_all]. Ces méthodes fonctionnement de manière similaire à celles de sauvegarde détaillées plus haut à l'exception du fait que [ORM::delete] peut prendre en argument l'`id` de l'enregistrment à supprimer. \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/fr-fr/tutorials.urls.md b/includes/kohana/modules/userguide/guide/fr-fr/tutorials.urls.md new file mode 100644 index 00000000..911a21a4 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/fr-fr/tutorials.urls.md @@ -0,0 +1,163 @@ +# Routes, URLs et Liens + +Ce chapitre fournit les bases permettant de comprendre la logique de traitement des requêtes, de la génération des URLs et des liens. + +## Routage + +Comment évoqué dans le chapitre [processus de traitement des requêtes](about.flow), une requête est traitée par la classe [Request] qui tente de trouver une [Route] correspondante et charge les méthodes appropriées du controleur qui permettront de traiter la requete. + +Si vous regardez le fichier `APPPATH/bootstrap.php` vous pouvez voir le code ci-dessous qui est exécuté juste avant que la requête ne soit traitée par [Request::instance]: + + Route::set('default', '((/(/)))') + ->defaults(array( + 'controller' => 'welcome', + 'action' => 'index', + )); + +Ce code crée une route appelée `default` dont l'URI doit avoir le format `((/(/)))`. Les éléments entourés par `<>` sont des *clés* et ceux entourés par `()` définissent les parties *optionnelles* de l'URI. Dans le code ci-dessus , l'URI entière est optionnelles ce qui signifie que même une URI vide serait traitée en utilisant les valeurs par défaut spécifiées dans la route. Cela se traduirait par le chargement de la classe `Controller_Welcome` et l'exécution de sa méthode `action_index` pour traiter la requête. + +A noter que les routes de Kohana peuvent contenir tous caractères exceptés `()<>`. Dans la route ci-dessus le caractère `/` est utilisé comme séparateur mais tant que l'expression matche l'URI demandée il n'y a aucune restriction sur le format des routes. + +### Répertoires + +Par soucis d'organisation, il est commun de vouloir organiser certains de vos controleurs dans des sous-répertoires. Par exemple pour grouper votre section d'administration (tout vos controleurs d'administration) de votre site dans un sous-répertoire admin: + + Route::set('admin', 'admin(/(/(/)))') + ->defaults(array( + 'directory' => 'admin', + 'controller' => 'home', + 'action' => 'index', + )); + +Cette route indique qu'il faut que l'URI commence obligatoirement par `admin` pour matcher. Le sous-répertoire est statiquement assigné à `admin` dans les paramètres par défaut. De cette manière, la requête `admin/users/create` chargera la classe `Controller_Admin_Users` et appellera la méthode `action_create`. + +### Expressions régulières + +Le système de routage de Kohana utilise des expressions régulière compatible Perl. Par défaut les clés (entourées par `<>`) sont matchées par l'expression `[a-zA-Z0-9_]++` mais vous pouvez définir vos propres expressions pour chacunes des clés en passant un tableau associatif de clés et d'expressions comme paramètre additionnel de la méthode [Route::set]. + +Par exemple, imaginons qu'en plus d'une section administration, votre site contient une section blog dont les controleurs sont situés dans un sous-répertoire blog. Alors vous pouvez soit écrire 2 routes distinctes ou bien tout simplement faire: + + Route::set('sections', '(/(/(/)))', + array( + 'directory' => '(admin|blog)' + )) + ->defaults(array( + 'controller' => 'home', + 'action' => 'index', + )); + +Cette route vous permet donc d'avoir 2 sections, 'admin' et 'blog' et d'organiser les controleurs dans des sous-répertoires distincts. + +### Exemples de routes + +Les possibilités sont bien sûres infinies, néanmoins voici quelques exemples courants: + + /* + * Raccourcis d'authentification + */ + Route::set('auth', '', + array( + 'action' => '(login|logout)' + )) + ->defaults(array( + 'controller' => 'auth' + )); + + /* + * Feeds multi-formats + * 452346/comments.rss + * 5373.json + */ + Route::set('feeds', '(/).', + array( + 'user_id' => '\d+', + 'format' => '(rss|atom|json)', + )) + ->defaults(array( + 'controller' => 'feeds', + 'action' => 'status', + )); + + /* + * Pages statiques + */ + Route::set('static', '.html', + array( + 'path' => '[a-zA-Z0-9_/]+', + )) + ->defaults(array( + 'controller' => 'static', + 'action' => 'index', + )); + + /* + * Vous n'aimez pas les slashes? + * EditGallery:bahamas + * Watch:wakeboarding + */ + Route::set('gallery', '():', + array( + 'controller' => '[A-Z][a-z]++', + 'action' => '[A-Z][a-z]++', + )) + ->defaults(array( + 'controller' => 'Slideshow', + )); + + /* + * Recherche rapide + */ + Route::set('search', ':', array('query' => '.*')) + ->defaults(array( + 'controller' => 'search', + 'action' => 'index', + )); + +Les Routes sont évaluées dans l'odre dans lequel elles sont définies. C'est pour cette raison que la route par défaut est définie à la fin de sorte que les routes spécifiques soient testées avant. + +De plus cela implique qu'il faut faire attention si vous définissez des routes après le chargement des modules, car les routes incluses dans ceux-ci pourrait entrer en conflit. + +### Paramétres des requêtes + +Le répertoire (directory), le controleur (controller) et l'action sont accessibles à travers l'instance [Request] d'une des 2 manières suivantes: + + $this->request->action; + Request::instance()->action; + +Toutes les autres clés spécifiées dans vos routes sont accessibles en utilisant: + + $this->request->param('key_name'); + +La méthode [Request::param] peut prendre un second paramètre optionnel permettant de spécifier une valeur par défaut à retourner au cas où la clé n'est pas affectée par la route. Si aucun argument n'est passé, toutes les clés sont passés sous forme d'un tableau associatif. + +### Convention + +La convention est de placer toutes vos routes dans le fichier `MODPATH//init.php` si elles concernent un module et sinon, si elles sont spécifiques à l'application, il faut tout simplement les ajouter au fichier `APPPATH/bootstrap.php` au-dessus de la route par défaut. Bien sûr cela ne vous empêche pas de les inclure depuis un fichier externe ou de les générer dynamiquement. + +## URLs + +Outre les capacités puissantes de gestion des routes de Kohana, Kohana fournit aussi des méthodes de génération d'URLs pour vos routes. Vous pouvez bien sûr spécifier des URIs en utilisant [URL::site] pour créer une URL complète: + + URL::site('admin/edit/user/'.$user_id); + +Cependant, Kohana fournit aussi une méthode permettant de générer les URIs à partir de la définition des routes que vous avez écrites. C'est extrêmement utile si vos routes sont amenées à changer car vous n'aurez pas à remodifier votre code partout où vous avez spécifié des URIs comme ci-dessus. Voici un exemple de génération dynamique qui correspond à la route `feeds` définie dans la liste d'exemples plus haut: + + Route::get('feeds')->uri(array( + 'user_id' => $user_id, + 'action' => 'comments', + 'format' => 'rss' + )); + +Imaginez que plus tard vous décidez de changer la définition de la route en `feeds/(/).`. Avec le code ci-dessus l'URI générée est toujours valide après ce changement! Lorsqu'une partie de l'URI est entourée de paranthèses et qu'elle représente une clé qui n'est pas fournie dans la génération de l'URI et qui n'a pas de valeur par défaut alors cette partie est enlevée de l'URI. C'est le cas de la partie `(/)` de la route par défaut; elle ne sera pas incluse dans l'URI générée si l'id n'est pas fourni. + +Une autre méthode pratique est [Request::uri] qui fait la même chose que la précédente méthode excepté qu'elle utilise la route courante. Si la route courante est la route par défaut dont l'URI est `users/list`, il est possible d'écrire le code suivant pour générer des URIs au format `users/view/$id`: + + $this->request->uri(array('action' => 'view', 'id' => $user_id)); + +Au sein d'une vue il est préferrable d'utiliser: + + Request::instance()->uri(array('action' => 'view', 'id' => $user_id)); + +## Liens + +[!!] links stub diff --git a/includes/kohana/modules/userguide/guide/he-il/about.autoloading.md b/includes/kohana/modules/userguide/guide/he-il/about.autoloading.md new file mode 100644 index 00000000..46064bb2 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/he-il/about.autoloading.md @@ -0,0 +1,18 @@ +# Autoloading - טעינה אוטומטית + +Kohana יודע לנצל את יכולת הטעינה אוטומטית של PHP [autoloading](http://php.net/manual/language.oop5.autoload.php). +עובדה זו מבטלת את הצורך בשימוש ב [include](http://php.net/include) או [require](http://php.net/require) לפני השימוש בבקר. + +הבקרים (Classes) נטענים על ידי מטודת [Kohana::auto_load], אשר יודעת לעשות את ההמרה משם בקר לשם קובץ: + +1. בקרים צריכים להיות ממוקמים בתוך תקיית `classes/` השייכים ל [filesystem](about.filesystem) +2. כל קו תחתי בשם הבקר יהפוך לסלאש '/' ויחפש בתת תקיות בהתאם +3. שם הקובץ צריך להיות כתוב באותיות קטנות + +כאשר קוראים לבקר שלא נטען (לדוגמא: `Session_Cookie`) קוהנה תחפש בעזרת פקודת [Kohana::find_file] את הקובץ `classes/session/cookie.php`. + +## Custom Autoloaders - טעינה אוטומטית מותאמת אישית + +[!!] הגדרת ברירת המחדל של הטעינה האוטומטית נמצאת בקובץ `application/bootstrap.php`. + +בקרים נוספים ניתן להוסיף ע"י שימוש ב [spl_autoload_register](http://php.net/spl_autoload_register). \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/he-il/about.configuration.md b/includes/kohana/modules/userguide/guide/he-il/about.configuration.md new file mode 100644 index 00000000..6db0d4ae --- /dev/null +++ b/includes/kohana/modules/userguide/guide/he-il/about.configuration.md @@ -0,0 +1,99 @@ +# General Configuration - הגדרות כלליות + +[!!] Finish translating... (todo: description of benefits of static properties for configuration) + +## Core Configuration - הגדרות בסיסיות + +ההגדרה הראשונה אותה יש לבצע בכל התקנה של קוהנה היא שינוי ההגדרות של [Kohana::init] ב `application/bootstrap.php`. +ההגדרות הן: + +שגיאות: +האם להשתמש בטיפול שגיאות ויוצאי דופן פנימי של הקוהנה +ערך ברירת מחדל - True, יש לשנות ל FLASE במידה ולא מעוניינים + +פרופיל: +האם להשתמש בדף הפרופיל הסטטיסטי +ערך ברירת מחדל - True +יש לשנות ל FALSE במידה ולא מעוניינים - מומלץ שלא להשתמש באפשרות זו בגרסה הסופית על מנת להסתיר מידע רגיש וטעינה מהירה יותר של הדפים + +caching - זכרון מטמון +האם לשמור בזכרון מטמון את המיקום של הקבצים בין בקשות? +ערך ברירת מחדל - True, יש לשנות ל FALSE במידה ולא מעוניינים +פעולה זו מגבירה באופן דרמטי את מהירות הטעינת דפים [Kohana::find_file] ולכן יכולה להיות בעלת השפעה גדולה על רמת הביצועים הכללית של האפליקציה. +חשוב להשתמש באופצייה זו רק בגרסה הסופית או בשביל נסיונות. + +`string` charset +: Character set used for all input and output. (Default `"utf-8"`) Should be a character set that is supported by both [htmlspecialchars](http://php.net/htmlspecialchars) and [iconv](http://php.net/iconv). + +`string` base_url +: Base URL for the application. (Default `"/"`) Can be a complete or partial URL. For example "http://example.com/kohana/" or just "/kohana/" would both work. + +`string` index_file +: The PHP file that starts the application. (Default `"index.php"`) Set to `FALSE` when you remove the index file from the URL with URL rewriting. + +`string` cache_dir +: Cache file directory. (Default `"application/cache"`) Must point to a **writable** directory. + +## Cookie Settings + +There are several static properties in the [Cookie] class that should be set, particularly on production websites. + +`string` salt +: Unique salt string that is used to enable [signed cookies](security.cookies) + +`integer` expiration +: Default expiration lifetime in seconds + +`string` path +: URL path to restrict cookies to be accessed + +`string` domain +: URL domain to restrict cookies to be accessed + +`boolean` secure +: Only allow cookies to be accessed over HTTPS + +`boolean` httponly +: Only allow cookies to be accessed over HTTP (also disables Javascript access) + +# Configuration Files + +Configuration is done in plain PHP files, which look similar to: + +~~~ + 'value', + 'options' => array( + 'foo' => 'bar', + ), +); +~~~ + +If the above configuration file was called `myconf.php`, you could acess it using: + +~~~ +$config = Kohana::config('myconf'); +$options = $config['options']; +~~~ + +[Kohana::config] also provides a shortcut for accessing individual keys from configuration arrays using "dot paths". + +Get the "options" array: + +~~~ +$options = Kohana::config('myconf.options'); +~~~ + +Get the "foo" key from the "options" array: + +~~~ +$foo = Kohana::config('myconf.options.foo'); +~~~ + +Configuration arrays can also be accessed as objects, if you prefer that method: + +~~~ +$options = Kohana::config('myconf')->options; +~~~ diff --git a/includes/kohana/modules/userguide/guide/he-il/about.filesystem.md b/includes/kohana/modules/userguide/guide/he-il/about.filesystem.md new file mode 100644 index 00000000..6372ee09 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/he-il/about.filesystem.md @@ -0,0 +1,18 @@ +# Cascading Filesystem - מערכת קבצים מדורגת + +מערכת הקבצים של Kohana בנוייה ממבנה בסיסי יחיד אשר משוכפל לכל התקיות הנמצאות בנתיב המכונה +include path. להלן צורת המבנה: + +1. application - אפליקציה +2. modules, in order added - מודולים, לפי סדר ההופעה +3. system - מערכת + +קבצים הנמצאים בתקיות שמעל ה include path מקבלים קדימות על קבצים עם שם זהה, עובדה המאפשרת +לטעון ולדרוס פעולות קבצים על ידי טעינה של קבצים זהים לקבצים הקיימים, רק במיקום גבוה יותר יחסית. לדוגמא: + +![Cascading Filesystem Infographic](img/cascading_filesystem.png) + +אם יש מצב בו יש לנו קובץ מבט (view) בשם layout.php הממוקם בתקייה application/views וגם קיים בתקייה system/views +הקובץ הממוקם בתקייה application יהיה זה שיוחזר ברגע שננסה לגשת ל layout.php +בגלל שהוא נמצא גבוה יותר בסדר האינקלוד (include path order). +ואם נמחוק את הקובץ הנמצא בתקייה application/views, אז הקובץ שיוחזר יהיה הקובץ השני שממוקם ב system/views. \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/he-il/about.flow.md b/includes/kohana/modules/userguide/guide/he-il/about.flow.md new file mode 100644 index 00000000..a94d08bb --- /dev/null +++ b/includes/kohana/modules/userguide/guide/he-il/about.flow.md @@ -0,0 +1,22 @@ +# Request Flow - זרימת תהליך בקשה מהשרת + +כל אפליקציה שרצה על קוהנה עוברת תהליך זהה בעת ביצוע בקשה של טעינת דף מהשרת + +1. האפליקציה נטענת ע"י הרצת הדף הראשי `index.php` +2. מכלילה בתוכה את הדף `APPPATH/bootstrap.php` +3. ה bootstrap קורא ל [Kohana::modules] עם רשימה של המודולים שבשימוש + 1. נוצר מערך עם הנתיבים של כל התקיות והקבצים המכילים את המודול + 2. בדיקה האם למודול יש קובץ init.php ובמידה וכן לטעון אותו + * כל קובץ init.php יכול לכלול בתוכו routes (ניתובים) חדשים אשר נטענים למערכת +4. [Request::instance] רץ על מנת לבצע את הקריאה + 1. בדיקה מול ה routes הקיימים על מנת למצוא את המתאים + 2. טעינה של בקר (controller) והעברת הבקשה אליו + 3. קריאה לפונקציה [Controller::before] של הבקר המתאים + 4. קריאה לפעולה של הבקר לפי ה route + 5. קריאה לפונקציה [Controller::after] של הבקר המתאים +5. הצגה של התוצאה + + +יש אפשרות לשנות את אופן פעולת הבקר עצמו על ידי הפונקציה [Controller::before] בהסתמך על המשתנים בבקשה + +[!!] Stub diff --git a/includes/kohana/modules/userguide/guide/he-il/about.kohana.md b/includes/kohana/modules/userguide/guide/he-il/about.kohana.md new file mode 100644 index 00000000..7103c9d3 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/he-il/about.kohana.md @@ -0,0 +1,21 @@ +# מה זה Kohana? + +Kohana היא מערכת בקוד פתוח, + [תשתית פיתוח לרשת](http://wikipedia.org/wiki/Web_Framework) +[מונחה עצמים](http://wikipedia.org/wiki/Object-Oriented_Programming) [MVC](http://wikipedia.org/wiki/Model-View-Controller "Model View Controller") +שנבנתה בשימוש עם +[PHP5](http://php.net/manual/intro-whatis "PHP Hypertext Preprocessor") +ע"י צוות מתנדבים שמטרתה להיות מהירה, מאובטחת, וקטנה. + +[!!] Kohana רשומה תחת רישיון ה [BSD license](http://kohanaframework.org/license), אי לכך באפשרותך לעשות כל שימוש באם הוא קוד פתוח, מסחרי, או פרוייקט אישי בלי שום מגבלות משפטיות. + +## מה עושה את Kohana כל-כך מצויין? + + +Anything can be extended using the unique [filesystem](about.filesystem) design, little or no [configuration](about.configuration) is necessary, [error handling](debugging.errors) helps locate the source of errors quickly, and [debugging](debugging) and [profiling](debugging.profiling) provide insight into the application. + +To help secure your applications, tools for [XSS removal](security.xss), [input validation](security.validation), [signed cookies](security.cookies), [form](security.forms) and [HTML](security.html) generators are all included. The [database](security.database) layer provides protection against [SQL injection](http://wikipedia.org/wiki/SQL_Injection). Of course, all official code is carefully written and reviewed for security. + +## המדריך הזה מעפן! + +We are working very hard to provide complete documentation. If you are having trouble finding an answer, check the [unofficial wiki](http://kerkness.ca/wiki/doku.php). If you would like to add or change something in the guide, please [fork the userguide](http://github.com/kohana/userguide), make your changes, and send a pull request. If you are not familar with git, you can also submit a [feature request](http://dev.kohanaframework.org/projects/kohana3/issues) (requires registration). diff --git a/includes/kohana/modules/userguide/guide/he-il/about.mvc.md b/includes/kohana/modules/userguide/guide/he-il/about.mvc.md new file mode 100644 index 00000000..131e43bd --- /dev/null +++ b/includes/kohana/modules/userguide/guide/he-il/about.mvc.md @@ -0,0 +1,29 @@ +# מודל Model View Controller + +תבנית Model-View-Controller (בקיצור MVC) היא תבנית עיצוב בהנדסת תוכנה המשמשת להפשטת יישום כלשהו. התבנית מתארת טכניקה לחלוקת היישום לשלושה חלקים, מודל, מבט ובקר, המחוברים ביניהם בצימוד רפוי מונחה אירועים. בדרך זו, התלות הדדית בין ממשק המשתמש לשאר חלקי התוכנה פוחתת, ואת החלקים השונים ניתן לפתח באופן בלתי-תלוי. בנוסף, קל יותר לתחזק את התוכנה וכן לעשות שימוש חוזר בחלקי היישום שהופרדו. + +#תיאור התבנית + +מקובל לחלק יישום תוכנה למספר שכבות נפרדות: שכבת התצוגה (ממשק משתמש), שכבת התחום העסקי (לעתים נקראת גם "שכבת הלוגיקה העסקית") ושכבת הגישה לנתונים. בתבנית MVC, שכבת התצוגה מחולקת בנוסף למבט ובקר. יש המחשיבים את התבנית כתבנית עיצוב, אך בהשוואה לתבניות עיצוב אחרות, MVC עוסקת במבנים בקנה מידה בינוני-גדול ולכן נחשבת גם כתבנית ארכיטקטורה. + +מודל + המודל הוא יצוג מסוים, מוכוון תחום עסקי, של המידע עליו פועל היישום. המודל, למרות הדעה הרווחת, אינו שם אחר לשכבת התחום העסקי והוא נפרד ממנה. תבנית MVC אינה מזכירה במפורש את שכבת הגישה לנתונים, מכיוון ששכבה זו היא מתחת למודל, או נעטפת על ידו. + +מבט + תפקידו להמיר את נתוני המודל לייצוג המאפשר למשתמש לבצע פעולת גומלין כלשהי. לרוב מדובר על המרה לממשק משתמש כלשהו. תבנית MVC משמשת רבות ביישומי Web, בהם המבט הוא דף HTML והקוד האוסף מידע דינמי לדף. + +בקר + תפקידו לעבד ולהגיב לאירועים המתרחשים במבט, לרוב, כתגובה לפעולה של המשתמש. בעיבוד האירועים, הבקר עשוי לשנות את המידע במודל, באמצעות שפעול שירותים המוגדרים בו. בקרים מורכבים מתבססים לרוב על יישום של תבנית Command. + + +#אופן הפעולה + +ניתן ליישם את תבנית העיצוב MVC בדרכים רבות, אך לרוב היא מיושמת כך: + +- הבקר נרשם כ-Event Handler או Callback במבט, בדרך כלל סמוך ליצירת הבקר. כלומר, יישום של שיטת היפוך הפיקוח (IoC). משמע, הבקר יקבל פיקוח כאשר יתרחש אירוע קלט בממשק המשתמש. +- המשתמש מבצע פעולת גומלין כלשהי עם הממשק. לדוגמה, מקליק על כפתור 'הוסף מוצר לעגלה'. +- הבקר שנרשם על המבט מקבל פיקוח ומשפעל שירותים המוגדרים במודל, כדי לשקף את הפעולה שביצע המשתמש. לדוגמה, עדכון 'עגלת הקניות' של המשתמש בפריט נוסף. +- המבט מקבל בצורה עקיפה את החוכמת התצוגה שלו מהבקר, בדרך כלל באמצעות יישום של תבנית Strategy. +- המבט משתמש במודל כדי ליצור את ממשק המשתמש. לדוגמה, המבט מפיק רשימה של הפריטים בעגלה, כפי שאלה מיוצגים כרגע במודל. בין השניים אין קשר הדוק, והמודל אינו מודע לכך שהמבט ניגש למידע המאוחסן בו. +- לעתים, המודל עשוי להודיע על שינויים המתחוללים בו לצדדים שלישיים נוספים, בדרך כלל באמצעות יישום של תבנית Observer. +- ממשק המשתמש ממתין לפעולות נוספות של המשתמש, וכשאלה מתרחשות, התהליך חוזר על עצמו. diff --git a/includes/kohana/modules/userguide/guide/he-il/debugging.code.md b/includes/kohana/modules/userguide/guide/he-il/debugging.code.md new file mode 100644 index 00000000..edeb38db --- /dev/null +++ b/includes/kohana/modules/userguide/guide/he-il/debugging.code.md @@ -0,0 +1,24 @@ +# Debugging - דיבוג + +קוהנה כוללת מספר כלים חזקים על מנת לעזור לך לדבג את האפליקציה שלך. +הכלי הבסיסי הוא [Kohana::debug]. +כלי זה יציג את כל המשתנים או משתנה מסויים מכל סוג שהוא, בדומה ל [var_export](http://php.net/var_export) או [print_r](http://php.net/print_r), רק שקוהנה יודעת להשתמש ב HTML להצגה נוחה יותר + +~~~ +// הצג נתונים אודות המשתנים $foo ו- $bar +echo Kohana::debug($foo, $bar); +~~~ + +קוהנה גם מאפשרת בקלות לצפות בקוד המקור של קובץ מסויים ע"י שימוש ב [Kohana::debug_source]. + +~~~ +// הצגה של שורה מסויימת מקובץ מסויים +echo Kohana::debug_source(__FILE__, __LINE__); +~~~ + +במידה ואתה מעוניין להציג מידע על האפליקציה מבלי לחשוף את התקיית התקנה, ניתן להשתמש ב [Kohana::debug_path]: + +~~~ +// מציג "APPPATH/cache" במקום הנתיב האמיתי +echo Kohana::debug_file(APPPATH.'cache'); +~~~ diff --git a/includes/kohana/modules/userguide/guide/he-il/debugging.errors.md b/includes/kohana/modules/userguide/guide/he-il/debugging.errors.md new file mode 100644 index 00000000..2369aea0 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/he-il/debugging.errors.md @@ -0,0 +1,29 @@ +# Error/Exception Handling - טיפול בשגיאות וחריגים + +Kohana מאפשרת לנו טיפול נוח בשגיאות וחריגים על ידי הפיכת השגיאות לחריגים בעזרת ה +[ErrorException](http://php.net/errorexception) של PHP. +Kohana יודעת להציג נתונים רבים אודות השגיאות והחריגים שזיהתה: + +1. Exception class - +2. Error level - רמת השגיאה +3. Error message - הודעת שגיאה +4. Source of the error, with the error line highlighted - מקור השגיאה עם סימון השורה הבעייתית +5. A [debug backtrace](http://php.net/debug_backtrace) of the execution flow - אפשרות מעקב אחורנית אודות הקריאות השונות שבוצעו עד לקבלת השגיאה על מנת לעקוב לאחור אחר מקור השגיאה +6. Included files, loaded extensions, and global variables - קבצים שנכללו, סיומות שנטענו ומשתנים גלובאלים + +## דוגמא להודעת שגיאה + +לחץ על אחד הקישורים הממוספרים על מנת להציג או להסתיר את המידע הנוסף + +
    {{userguide/examples/error}}
    + +## Disabling Error/Exception Handling - ביטול הטיפול בשגיאות וחריגים + +במידה וברצונך לבטל את הטיפול בשגיאות, ניתן לעשות זאת בעת הקריאה + [Kohana::init] בצורה הבאה: + +~~~ +Kohana::init(array('errors' => FALSE)); +~~~ + +חשוב לזכור שבדרך כלל נרצה שהשגיאות המפורטות יופיעו רק בעבודה לוקאלית ולא באתר אונליין \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/he-il/debugging.profiling.md b/includes/kohana/modules/userguide/guide/he-il/debugging.profiling.md new file mode 100644 index 00000000..bd7663b1 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/he-il/debugging.profiling.md @@ -0,0 +1,21 @@ +# Profiling - פרופיל סטטיסטי לכל דף + +קוהנה מאפשרת בקלות לצפות בסטטיסטיקה אודות האפליקציה: + +1. קריאות למטודות קוהנה +2. בקשות +3. שאילתות שבוצעו על מסד הנתונים +4. ממוצע זמני פעולה של האפליקציה + +## דוגמא + +ניתן לאסוף ולהציג את הסטטיסטיקות בכל רגע נתון: +~~~ +
    + +
    +~~~ + +## התוצאה: + +{{profiler/stats}} \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/he-il/menu.md b/includes/kohana/modules/userguide/guide/he-il/menu.md new file mode 100644 index 00000000..5b027976 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/he-il/menu.md @@ -0,0 +1,26 @@ +1. **מתחילים** + - [ מה זה Kohana?](about.kohana) + - [מוסכמות וסיגנון](about.conventions) + - [התקנה](about.install) + - [שידרוג](about.upgrading) + - [הגדרות](about.configuration) + - [Model View Controller הסבר על](about.mvc) + - [מערכת קבצים](about.filesystem) + - [Autoloading - טעינה אוטומטית](about.autoloading) + - [Request זרימת](about.flow) + - [API דפדפן](api) +2. **ערכות לימוד** + - [Hello, World](tutorials.helloworld) + - [Routes, URLs, and Links](tutorials.urls) + - [Databases](tutorials.databases) + - [ORM](tutorials.orm) + - [עבודה עם Git](tutorials.git) +3. **אבטחה** + - [XSS](security.xss) + - [Validation - ואלידציה](security.validation) + - [Cookies - עוגיות](security.cookies) + - [Database - מסד נתונים](security.database) +4. **ניפוי באגים** + - [קוד](debugging.code) + - [טיפול בשגיאות](debugging.errors) + - [פרופיל](debugging.profiling) diff --git a/includes/kohana/modules/userguide/guide/menu.md b/includes/kohana/modules/userguide/guide/menu.md new file mode 100644 index 00000000..a5aacb86 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/menu.md @@ -0,0 +1,31 @@ +1. **Getting Started** + - [What is Kohana?](about.kohana) + - [Conventions and Style](about.conventions) + - [Model View Controller](about.mvc) + - [Cascading Filesystem](about.filesystem) + - [Request Flow](about.flow) + - [Installation](about.install) + - [Upgrading](about.upgrading) + - [API Browser](api) +3. **Basic Usage** + - [Configuration](using.configuration) + - [Loading Classes](using.autoloading) + - [Views and HTML](using.views) + - [Sessions and Cookies](using.sessions) + - [Messages](using.messages) +4. **Debugging** + - [Code](debugging.code) + - [Error Handling](debugging.errors) + - [Profiling](debugging.profiling) +5. **Security** + - [XSS](security.xss) + - [Validation](security.validation) + - [Cookies](security.cookies) + - [Database](security.database) +6. **Tutorials** + - [Hello, World](tutorials.helloworld) + - [Routes, URLs, and Links](tutorials.urls) + - [Clean URLs](tutorials.removeindex) + - [Databases](tutorials.databases) + - [ORM](tutorials.orm) + - [Working with Git](tutorials.git) diff --git a/includes/kohana/modules/userguide/guide/nl/about.conventions.md b/includes/kohana/modules/userguide/guide/nl/about.conventions.md new file mode 100644 index 00000000..65094928 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/about.conventions.md @@ -0,0 +1,316 @@ +# Conventies + +Het is aanbevolen om Kohana's [manier van coderen](http://dev.kohanaframework.org/wiki/kohana2/CodingStyle) te gebruiken. Dit gebruikt de [BSD/Allman stijl](http://en.wikipedia.org/wiki/Indent_style#BSD.2FAllman_style) van haakjes, en nog andere dingen. + +## Class namen en locaties van bestanden {#classes} + +Class namen in Kohana volgen een strikte conventie om [autoloading](using.autoloading) gemakkelijker te maken. Class namen zouden met een hoofdletter moeten beginnen en een underscore gebruiken om woorden af te scheiden van elkaar. Underscores zijn belangrijk omdat ze de locatie van het bestand weerspiegelen in de folderstructuur. + +De volgende conventies worden gebruikt: + +1. CamelCased class namen worden niet gebruikt, alleen maar als het onnodig is om een nieuw folderniveau aan te maken. +2. Alle class bestandsnamen en foldernamen zijn met kleine letters geschreven. +3. Alle classes zitten in de `classes` folder. Dit kan op ieder niveau in het [cascading filesystem](about.filesystem). + +[!!] In tegenstelling tot Kohana v2.x, is er geen afscheiding tussen "controllers", "models", "libraries" en "helpers". Alle classes worden in de folder "classes/" geplaatst, of het nu static "helpers" of object "libraries" zijn. Ieder design pattern is mogelijk voor het maken van classes: static, singleton, adapter, etc. + +## Voorbeelden + +Onthoud dat in een class, een underscore een folder betekent. Bekijk de volgende voorbeelden: + +Class Naam | Locatie File +----------------------|------------------------------- +Controller_Template | classes/controller/template.php +Model_User | classes/model/user.php +Database | classes/database.php +Database_Query | classes/database/query.php +Form | classes/form.php + +## Coding Standaarden {#coding_standards} + +Om zeer consistente broncode te schrijven, vragen we dat iedereen de coding standaarden zo nauw mogelijk probeert na te volgen. + +### Gekrulde Haakjes (Brackets) + +Gebruik aub [BSD/Allman Stijl](http://en.wikipedia.org/wiki/Indent_style#BSD.2FAllman_style) van bracketing. + +### Naam conventies + +Kohana gebruikt underscore namen, geen camelCase. + +#### Classes + + 5) ? ($bar + $foo) : strlen($bar); + +Bij het scheiden van complexe ternaries (ternaries waarbij het eerste deel meer dan ~80 karakters bevat) in meerdere regels, moet je spaties gebruiken om operators op te lijnen, deze plaats je in het begin van de opeenvolgende lijnen: + + $foo = ($bar == $foo) + ? $foo + : $bar; + +### Type Casting + +Type casting wordt gedaan met spatie langs elke kant van de cast: + + // Correct: + $foo = (string) $bar; + if ( (string) $bar) + + // Niet correct: + $foo = (string)$bar; + +Indien mogelijk, gebruik dan in plaats van type casting ternaire operators: + + // Correct: + $foo = (bool) $bar; + + // Niet correct: + $foo = ($bar == TRUE) ? TRUE : FALSE; + +Bij het casten van een integer of een boolean gebruik je het korte formaat: + + // Correct: + $foo = (int) $bar; + $foo = (bool) $bar; + + // Incorrect: + $foo = (integer) $bar; + $foo = (boolean) $bar; + +### Constanten + +Gebruik altijd hoofdletters voor constanten: + + // Correct: + define('MY_CONSTANT', 'my_value'); + $a = TRUE; + $b = NULL; + + // Niet correct: + define('MyConstant', 'my_value'); + $a = True; + $b = null; + +Plaats constant vergelijkingen aan het einde van de tests: + + // Correct: + if ($foo !== FALSE) + + // Niet correct: + if (FALSE !== $foo) + +Dit is een enigszins een controversiële keuze, dus is een uitleg op zijn plaats. Als we het vorige voorbeeld in gewoon taal schrijven, zou het goede voorbeeld als volgt te lezen zijn: + + if variable $foo is not exactly FALSE + +En het foute voorbeeld zou als volgt te lezen zijn: + + if FALSE is not exactly variable $foo + +En aangezien we van links naar rechts lezen, is het logischer om de constante als laatste te plaatsen. + +### Commentaren + +#### Commentaren op één lijn + +Gebruik //, best boven de lijn met je code waar je de commentaar voor wilt schrijven. Laat een spatie tussen en start met een hoofdletter. Gebruik nooit # + + // Correct + + //Niet correct + // niet correct + # Niet correct + +### Reguliere expressies + +Bij het coderen van reguliere expressies gebruik je beter PCRE in plaats van POSIX. PCRE zou krachtiger en sneller zijn. + + // Correct: + if (preg_match('/abc/i'), $str) + + // Incorrect: + if (eregi('abc', $str)) + +Gebruik enkele aanhalingstekens rond uw reguliere expressies in plaats van dubbele aanhalingstekens. Enkele aanhalingstekens worden gemakkelijker door hun eenvoud in gebruik. In tegenstelling tot de dubbele aanhalingstekens ondersteunen ze niet variabele interpolatie, noch geïntegreerde backslash sequenties zoals \n of \t, enz. + + // Correct: + preg_match('/abc/', $str); + + // Incorrect: + preg_match("/abc/", $str); + +Bij het uitvoeren van een reguliere expressie zoeken en vervangen, gebruik dan de $n notatie voor terugverwijzingen. Dit verdient de voorkeur boven \\n. + + // Correct: + preg_replace('/(\d+) dollar/', '$1 euro', $str); + + // Incorrect: + preg_replace('/(\d+) dollar/', '\\1 euro', $str); + +Tot slot, let wel dat het $-teken voor de eindpositie van de lijn aan te geven toelaat om een newline-karakter als volgend karakter te gebruiken. Gebruik de D modifier om dit te verhelpen indien nodig. [Meer informatie](http://blog.php-security.org/archives/76-Holes-in-most-preg_match-filters.html). + + $str = "email@example.com\n"; + + preg_match('/^.+@.+$/', $str); // TRUE + preg_match('/^.+@.+$/D', $str); // FALSE diff --git a/includes/kohana/modules/userguide/guide/nl/about.filesystem.md b/includes/kohana/modules/userguide/guide/nl/about.filesystem.md new file mode 100644 index 00000000..f2a1ddee --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/about.filesystem.md @@ -0,0 +1,76 @@ +# Cascading Filesystem + +Het Kohana filesysteem heeft hiërarchie van folder-structuur. Wanneer een bestand wordt ingeladen door [Kohana::find_file], dan wordt het gezocht in de volgend volgorde: + +Application pad +: Gedefineerd als `APPPATH` in `index.php`. De standaard value hiervan is `application`. + +Module paden +: Dit is ingesteld als een associatieve array met behulp van [Kohana::modules] in `APPPATH/bootstrap.php`. Elk van de waarden van de array zal worden gezocht in de volgorde waarin de modules worden toegevoegd. + +System pad +: Gedefineerd als `SYSPATH` in `index.php`. De standaard value hiervan is `system`. Alle belangrijkste of "core"-bestanden en classes zijn hier gedefinieerd. + +Bestanden die zich hoger bevinden in de volgorde van het inladen van bestanden hebben voorrang op bestanden met dezelfde naam die zich lager bevinden in de volgorde van inladen, dit maakt het mogelijk om ieder bestand te overloaden door een bestand met dezelfde naam in een "hogere" folder te plaatsen: + +![Cascading Filesystem Infographic](img/cascading_filesystem.png) + +Als je een view bestand hebt met de naam `welcome.php` in de `APPPATH/views` en `SYSPATH/views` folders, dan zal hetgeen uit application worden gereturned als `welcome.php` wordt ingeladen omdat het "hoger" staat in de folderstructuur. + +## Types bestanden + +De top level folders van de application, module en systeem paden hebben volgende standaard folders: + +classes/ +: Alle classes dat je wilt [automatisch inladen](using.autoloading) moeten zich hier + bevinden. Dit houdt in controllers, models, en alle andere classes. Alle classes moeten + de [class naam conventies](about.conventions#classes) volgen. + +config/ +: Configuratie bestanden geven een associatieve array van opties terug die je kunt + inladen via [Kohana::config]. Zie [gebruik van configuratie](using.configuration) voor + meer informatie. + +i18n/ +: Vertalingsbestanden geven een associatieve array van strings terug. Vertalen wordt + gedaan door de `__()` methode te gebruiken. Om "Hello, world!" te vertalen in het + Spaans zou je de methode `__('Hello, world!')` oproepen met [I18n::$lang] ingesteld op "es-es". + Zie [gebruik van vertaling](using.translation) voor meer informatie. + +messages/ +: Berichtenbestanden geven een associatieve array van strings terug die ingeladen kunnen + worden via [Kohana::message]. Messages en i18n bestanden verschillen erin dat messages + niet worden vertaald, maar altijd geschreven worden in de standaard taal en verwezen worden + via een enkelvoudige key. Zie [gebruik van messages](using.messages) voor meer informatie. + +views/ +: Views zijn plain PHP files die worden gebruikt om HTML of een ander formaat te genereren. Het view bestand wordt + ingeladen in in een [View] object en toegewezen variabelen, die het dan zal omzetten naar een HTML fractie. Het is mogelijk om meerder views in elkaar te gebruiken. + Zie [gebruik van views](usings.views) voor meer informatie. + +## Vinden van betanden + +Het pad naar eender welk bestand in de folderstructuur kan worden gevonden door het gebruik van [Kohana::find_file]: + + // Vind het volledige pad naar "classes/cookie.php" + $path = Kohana::find_file('classes', 'cookie'); + + // Vind het volledige pad naar "views/user/login.php" + $path = Kohana::find_file('views', 'user/login'); + + +# Vendor Extensions + +Extensies die niet specifiek zijn aan Kohana noemen we "vendor" extensions. +Bijvoorbeeld, als je [DOMPDF](http://code.google.com/p/dompdf) wilt gebruiken, +dan moet je het kopiëren naar `application/vendor/dompdf` en de DOMPDF +autoloading class inladen: + + require Kohana::find_file('vendor', 'dompdf/dompdf/dompdf_config.inc'); + +Nu kan je DOMPDF gebruiken zonder inladen van andere bestanden: + + $pdf = new DOMPDF; + +[!!] Indien je views wilt omzetten in PDFs via DOMPDF, probeer dan de +[PDFView](http://github.com/shadowhand/pdfview) module. diff --git a/includes/kohana/modules/userguide/guide/nl/about.flow.md b/includes/kohana/modules/userguide/guide/nl/about.flow.md new file mode 100644 index 00000000..b9194e0d --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/about.flow.md @@ -0,0 +1,73 @@ +# Request Flow + +Iedere applicatie volgt de zelfde flow: + +1. Applicatie start vanaf `index.php`. +2. De application, module, en system paden worden ingesteld. +3. Error reporting niveaus worden ingesteld. +4. Install file wordt geladen, als het bestaat. +5. De [Kohana] class wordt ingeladen. +6. Het bootstrap bestand, `APPPATH/bootstrap.php`, wordt geinclude. +7. [Kohana::init] wordt aangeroepen, deze stelt error handling, caching, en logging in. +8. [Kohana_Config] lezers en [Kohana_Log] schrijvers worden toegevoegd. +9. [Kohana::modules] wordt aangeroepen om additionele modules te activeren. + * Module paden worden toegevoegd aan het [cascading filesystem](about.filesystem). + * Includen van `init.php` bestand, als het bestaat. + * Het `init.php` bestand kan een extra omgevingsinstellingen instellen, waaronder het toevoegen van routes. +10. [Route::set] wordt verschillende keren opgeroepen om de [applicaties routes](using.routing) te definiëren. +11. [Request::instance] wordt opgeroepen om het request-proces te starten. + 1. Iedere route controleren dat is ingesteld tot er een overeenkomst is gevonden. + 2. Conroller instantie wordt gecreeërd en het request wordt doorgeven eraan. + 3. De [Controller::before] methode wordt aangeroepen. + 4. De controller action wordt aangeroepen, deze genereerd de request response. + 5. De [Controller::after] methode wordt aangeroepen. + * De 5 bovenstaande stappen kunnen verschillende keren worden herhaald wanneer je [HMVC sub-requests](about.mvc) gebruikt. +12. De basis [Request] response wordt getoond + +## index.php + +Kohana volgt een [front controller] pattern, dit betekent dat alle requests worden gezonden naar `index.php`. Dit laat een zeer eenvoudig [bestandsstructuur](about.filesystem) design toe. In `index.php` zijn er enkele zeer basis configuratie opties mogelijk. je kan de `$application`, `$modules`, en `$system` paden veranderen en het error reporting level instellen. + +De `$application` variabele laat je toe om de folder in te stellen die al je application bestanden bevat. Standaard is dit `application`. De `$modules` variabele laat je toe om de folder in te stellen die alle module bestanden bevat. De `$system` variabele laat je toe om de folder in te stellen die alle Kohana bestanden bevat. + +Je kan deze drie folders overal naartoe verplaatsen. Bijvoorbeeld, als je folderstructuur zo is ingesteld: + + www/ + index.php + application/ + modules/ + system/ + +Dan kan je de folders uit de webroot verplaatsen: + + application/ + modules/ + system/ + www/ + index.php + +Dan moet je de instellingen in `index.php` veranderen naar: + + $application = '../application'; + $modules = '../modules'; + $system = '../system'; + +Nu kan geen enkele van deze folders worden bereikt via de webserver. Het is niet noodzakelijk om deze verandering te maken, maar het maakt het wel mogelijk om de folders te delen met meerdere applicaties, de mogelijkheden zijn enorm. + +[!!] Er is een veiligheidscontrole bovenaan elke Kohana file om te voorkomen dat het wordt uitgevoerd zonder het gebruik van de front controller. Maar natuurlijk is het veiliger om de application, modules, en system folders te verplaatsen naar een locatie dat niet kan worden bereikt via het web. + +### Error Reporting + +Standaard toont Kohana alle errors, zo ook strikte warnings. Dit wordt ingesteld door [error_reporting](http://php.net/error_reporting): + + error_reporting(E_ALL | E_STRICT); + +Als je applicatie live staat en in productie is, een meer conversatieve instelling is aangeraden, zoals het negeren van notices: + + error_reporting(E_ALL & ~E_NOTICE); + +Als je een wit scherm krijgt wanneer een error is opgetreden, dan zal uw host waarschijnlijk het tonen van errors hebben uitgeschakeld. Je kan dit terug aanzetten door deze lijn toe te voegen juist achter je `error_reporting` call: + + ini_set('display_errors', TRUE); + +Errors zouden **altijd** moeten worden getoond, zelf in productie, omdat het je toelaat om [exception en error handling](debugging.errors) te gebruiken om een mooie error pagina te tonen in plaats van een wit scherm als een error voorkomt. \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/nl/about.install.md b/includes/kohana/modules/userguide/guide/nl/about.install.md new file mode 100644 index 00000000..71a566d5 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/about.install.md @@ -0,0 +1,94 @@ +# Installatie + +1. Download de laatste **stabiele** release van de [Kohana website](http://kohanaframework.org/). +2. Unzip het gedownloade pakket om de `kohana` folder aan te maken. +3. Upload de inhoud van deze folder naar je webserver. +4. Open `application/bootstrap.php` en maak de volgende aanpassingen: + - Stel de standaard [timezone](http://php.net/timezones) in voor je applicatie + - Stel de `base_url` in de [Kohana::init] methode in om te verwijzen naar de locatie van de kohana folder op je server +6. Zorg ervoor dat de `application/cache` en `application/logs` folders schrijfrechten hebben voor de web server +7. Test je installatie door de URL te openen in je favoriete browser dat je hebt ingesteld als `base_url` + +[!!] Afhankelijk van je platform is het mogelijk dat de installatie subfolders hun rechten verloren hebben tijdens de zip extractie. Chmod ze allemaal met 755 door het commando `find . -type d -exec chmod 0755 {} \;` uit te voeren in de root van je Kohana installatie. + +Je zou de installatie pagina moeten zien. Als het errors toont, zal je ze moeten aanpassen vooraleer je verder kunt gaan. + +![Install Page](img/install.png "Voorbeeld van de installatie pagina") + +Eens je installatie pagina zegt dat je omgeving goed is ingesteld dan moet je de `install.php` pagina hernoemen of verwijderen in de root folder. Je zou nu de de Kohana welcome pagina moeten zien: + +![Welcome Page](img/welcome.png "Voorbeeld van welcome pagina") + +## Een productie-omgeving opzetten + +Er zijn enkele dingen dat je best doet met je applicatie vooraleer je deze in productie plaatst: + +1. Bekijk de [configuratie pagina](about.configuration) in de documentatie. + Dit omvat het grootste gedeelte van de globale instellingen dat zouden moeten veranderen bij andere omgevingen. + Als algemene regel, zet je best caching aan en zet je profiling uit ([Kohana::init] settings) voor sites in productie. + [Route caching](api/Route#cache) kan ook helpen als je heel wat routes hebt. +2. Catch alle exceptions in `application/bootstrap.php`, zodat gevoelige gegevens niet kan worden gelekt door stack traces. + Zie onderstaand voorbeeld van Shadowhand's [wingsc.com broncode](http://github.com/shadowhand/wingsc). +3. Zet APC of een andere soort opcode caching aan. Dit is het enige en eenvoudigste manier om de performantie te verbeteren dat je kunt doen in PHP zelf. Hoe complexer je applicatie, hoe groter het voordeel van opcode caching. + +[!!] Opmerking: De standaard bootstrap zal Kohana::$environment = $_ENV['KOHANA_ENV'] instellen indien ingesteld. Documentatie hoe je deze variable moet invullen kan je vinden in je webservers documentatie (e.g. [Apache](http://httpd.apache.org/docs/1.3/mod/mod_env.html#setenv), [Lighttpd](http://redmine.lighttpd.net/wiki/1/Docs:ModSetEnv#Options), [Nginx](http://wiki.nginx.org/NginxHttpFcgiModule#fastcgi_param))). Deze manier wordt als beste beschouwd in vergelijking met de alternatieve manieren om Kohana::$environment in te stellen. + + /** + * Stel de omgeving in aan de hand van het domein (standaard Kohana::DEVELOPMENT). + */ + Kohana::$environment = ($_SERVER['SERVER_NAME'] !== 'localhost') ? Kohana::PRODUCTION : Kohana::DEVELOPMENT; + /** + * Initialiseer Kohana op basis van de omgeving + */ + Kohana::init(array( + 'base_url' => '/', + 'index_file' => FALSE, + 'profile' => Kohana::$environment !== Kohana::PRODUCTION, + 'caching' => Kohana::$environment === Kohana::PRODUCTION, + )); + + /** + * Voer de algemene request uit met PATH_INFO. Als er geen URI is gespecifeerd, + * dan zal de URI automatisch worden gedetecteerd. + */ + $request = Request::instance($_SERVER['PATH_INFO']); + + try + { + // Propeer het request uit te voeren + $request->execute(); + } + catch (Exception $e) + { + if (Kohana::$environment == Kohana::DEVELOPMENT) + { + // Just re-throw the exception + throw $e; + } + + // De error loggen + Kohana::$log->add(Kohana::ERROR, Kohana::exception_text($e)); + + // Maak een 404 uitvoer + $request->status = 404; + $request->response = View::factory('template') + ->set('title', '404') + ->set('content', View::factory('errors/404')); + } + + if ($request->send_headers()->response) + { + // Verkrijg totaal aantal geheugen en snelheids tijd + $total = array( + '{memory_usage}' => number_format((memory_get_peak_usage() - KOHANA_START_MEMORY) / 1024, 2).'KB', + '{execution_time}' => number_format(microtime(TRUE) - KOHANA_START_TIME, 5).' seconds'); + + // Stel de totalen in, in de uitvoer + $request->response = str_replace(array_keys($total), $total, $request->response); + } + + + /** + * Toon de uitvoer dan het request. + */ + echo $request->response; diff --git a/includes/kohana/modules/userguide/guide/nl/about.kohana.md b/includes/kohana/modules/userguide/guide/nl/about.kohana.md new file mode 100644 index 00000000..1371b381 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/about.kohana.md @@ -0,0 +1,15 @@ +# Wat is Kohana? + +Kohana is een open source, [objectgeoriënteerd](http://nl.wikipedia.org/wiki/Objectgeori%C3%ABnteerd) [MVC](http://wikipedia.org/wiki/Model–View–Controller "Model View Controller") [web framework](http://wikipedia.org/wiki/Web_Framework) gebouwd met [PHP5](http://php.net/manual/intro-whatis "PHP Hypertext Preprocessor") door een aantal vrijwilligers die veiligheid, snelheid en een kleine voetafdruk nastreven. + +[!!] Kohana is gelicentieerd onder een [BSD license](http://kohanaframework.org/license), zodat je het framework legaal kunt gebruiken voor allerlei projecten: open source, commercieel of persoonlijk. + +## Waarom is Kohana zo goed? + +Alles kan worden uitgebreid door het unieke design van het [filesystem](about.filesystem), er is geen of weinig [configuratie](about.configuration) voor nodig, [error handling](debugging.errors) helpt je vlug de oorzaak te vinden van je fouten en [debuggen](debugging) en [profiling](debugging.profiling) zorgen voor een beter inzicht in je applicatie. + +Om je te helpen je applicatie te beveiligen zijn er tools voor [XSS te verwijderen](security.xss), [input validatie](security.validation), [gesigneerde cookies](security.cookies), [formulieren](security.forms) en [HTML](security.html) generators toegevoegd aan het systeem. De [database](security.database) layer voorkomt [SQL injectie](http://wikipedia.org/wiki/SQL_Injection). En natuurlijk, alle officiële code is met zorg geschreven en herbekeken inzake veiligheid. + +## Vul mee deze documentatie aan + +We zijn keihard en volop bezig om je van een complete documentatie te voorzien. Om deze documentatie te helpen verbeteren, gelieve dan de userguide te [forken](http://github.com/kohana/userguide), uw aanpassingen te doen en een pull request te sturen. Als je nog geen ervaring hebt met git kan je altijd een [feature request](http://dev.kohanaframework.org/projects/kohana3/issues) aanmaken (vereist registratie). \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/nl/about.mvc.md b/includes/kohana/modules/userguide/guide/nl/about.mvc.md new file mode 100644 index 00000000..2e94ba8f --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/about.mvc.md @@ -0,0 +1,7 @@ +# (Hiërarchische) Model View Controller + +Model View Controller (of MVC afgekort) is een populair design pattern dat de data (Model) afscheid van de presentatie/templates (View) en de request-logica (Controller). + +Het maakt ontwikkelen van applicaties een stuk gemakkelijker omdat het systeem zo gedesignd is om code meermaals te hergebruiken, wat wil zeggen dat je er minder moet schrijven! + +[!!] Stub diff --git a/includes/kohana/modules/userguide/guide/nl/about.translation.md b/includes/kohana/modules/userguide/guide/nl/about.translation.md new file mode 100644 index 00000000..00c3f529 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/about.translation.md @@ -0,0 +1,4 @@ +# Translation + +[!!] This article is a stub! + diff --git a/includes/kohana/modules/userguide/guide/nl/about.upgrading.md b/includes/kohana/modules/userguide/guide/nl/about.upgrading.md new file mode 100644 index 00000000..d843e996 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/about.upgrading.md @@ -0,0 +1,288 @@ +# Upgraden vanaf 2.3.x + +De code van Kohana v3 werkt grotendeels anders dan Kohana 2.3, hier is een lijst van de meeste valkuilen en tips om succesvol te upgraden. + +## Naming conventies + +In 2.x versies onderscheiden de verschillende soorten van classes (zoals controller, model, ...) zich met elkaar met behulp van achtervoegsels. Mappen in de model / controller mappen hadden geen invloed op de naam van de class. + +In 3.0 werd aanpak geschrapt ten gunste van de Zend framework bestandssysteem conventies, waar de naam van de class het pad is naar de class zelf, gescheiden door een underscore in plaats van slashes (dus `/some/class/file.php` bekomt `Some_Class_File`). +Zie de [conventies documentatie](start.conventions) voor meer informatie. + +## Input Library + +De Input Library is verwijderd in 3.0, er wordt nu aanbevolen om gewoon `$_GET` en `$_POST` te gebruiken. + +### XSS Protectie + +Als je invoer van gebruikers wilt filteren op XSS kan je [Security::xss_clean] gebruiken om: + + $_POST['description'] = security::xss_clean($_POST['description']); + +Je kan ook altijd [Security::xss_clean] gebruiken als filter met de [Validate] library: + + $validation = new Validate($_POST); + + $validate->filter('description', 'Security::xss_clean'); + +### POST & GET + +Eén van de grootste functies van de Input Library was als je probeerde een waarde uit een superglobale array te halen en deze bestond bestond niet, dan zou de Input Library een standaard waarde teruggeven dat je kon instellen: + + $_GET = array(); + + // $id heeft de waarde 1 gekregen + $id = Input::instance()->get('id', 1); + + $_GET['id'] = 25; + + // $id heeft de waarde 25 gekregen + $id = Input::instance()->get('id', 1); + +In 3.0 kan je deze functionaliteit nabootsen door [Arr::get] te gebruiken: + + $_GET = array(); + + // $id heeft de waarde 1 gekregen + $id = Arr::get($_GET, 'id', 1); + + $_GET['id'] = 42; + + // $id heeft de waarde 42 gekregen + $id = Arr::get($_GET, 'id', 1); + +## ORM Library + +Er zijn redelijk veel grote wijzingingen aangebracht in ORM sedert 2.3. Hier is een lijst met de meest voorkomende upgrade problemen. + +### Member variablen + +Alle member variablen hebben nu een voorvoegsel gekregen met een underscore (_) en zijn niet langer bereikbaar via `__get()`. Nu moet je een functie aanroepen met de naam van de property zonder de underscore. + +Bijvoorbeeld, in 2.3 had je `loaded` en in 3.x is dat nu `_loaded` en heb je nu toegang van buiten de class via `$model->loaded()`. + +### Relaties + +Als je in 2.3 de gerelateerde objecten van een model wilde herhalen, kon je dat zo doen: + + foreach($model->{relation_name} as $relation) + +Maar in 3.0 zal dit niet werken. In de 2.3 serie werden alle queries die gegenereerd werden met behulp van de Database Library gegeneeerd in een globale omgeving, wat betekent dat je niet kon proberen en maken van twee queries. Bijvoorbeeld: + +# TODO: GOED VOORBEELD!!!! + +Deze query zou mislukken doordat de tweede, inner query alle voorwaarden zou overerven van de eerste, wat zou leiden tot het mislukken. +In 3.0 is dit aangepast door iedere query te laten genereren in zijn eigen omgeving. Let wel dat sommige dingen hierdoor niet gaan werken zoals je verwacht. Bijvoorbeeld: + + foreach(ORM::factory('user', 3)->where('post_date', '>', time() - (3600 * 24))->posts as $post) + { + echo $post->title; + } + +[!!] (Zie [de Database tutorial](tutorials.databases) voor de nieuwe query syntax) + +In 2.3 zou je verwachten dat dit iterator teruggeeft van alle berichten van een gebruiker met `id` 3 met een `post_date` binnenin de 24 uren, maar in de plaats daarvan zal de WHERE conditie toegepast worden op het user-model en een `Model_Post` worden teruggevens met de joining conditities gespecifieerd. + +Om hetzelfde effect te verkrijgen zoals in 2.3, moet je de structuur iets aanpassen: + + foreach(ORM::factory('user', 3)->posts->where('post_date', '>', time() - (36000 * 24))->find_all() as $post) + { + echo $post->title; + } + +Dit is ook van toepassing op de `has_one` relaties: + + // Niet correct + $user = ORM::factory('post', 42)->author; + // Correct + $user = ORM::factory('post', 42)->author->find(); + +### Has and belongs to many relaties + +In 2.3 kon je `has_and_belongs_to_many` relaties specifieren. In 3.0 is deze functionaliteit herschreven naar `has_many` *through*. + +In het model definieer je een `has_many` relatie met een ander model maar dan voeg je nog een `'through' => 'table'` attribuut aan toe, waar `'table'` de naam is van de trough tabel. Bijvoorbeeld (in de context van posts<>categories): + + $_has_many = array + ( + 'categories' => array + ( + 'model' => 'category', // The foreign model + 'through' => 'post_categories' // The joining table + ), + ); + +Als je Kohana hebt opgezet om een tabel voorvoegsel te gebruiken, dan hoef je geen zorgen te maken om dit voorvoegsel hier te gebruiken bij de tabelnaam. + +### Foreign keys + +Als je in Kohana 2.x's ORM een foreign key wilde overschrijven moest je de relatie specificeren waaraan het toebehoorde, en de nieuwe foreign key instellen in de member variabele `$foreign_keys`. + +In 3.0 moet je nu een `foreign_key` definiëren in de relatie-definitie, zoals hier: + + Class Model_Post extends ORM + { + $_belongs_to = array + ( + 'author' => array + ( + 'model' => 'user', + 'foreign_key' => 'user_id', + ), + ); + } + +In dit voorbeeld zouden we een `user_id` veld moeten hebben in de tabel posts. + + + +In has_many relaties is de `far_key` het veld in de trough tabel die linkt naar de foreign tabel en is de `foreign key` het veld in de trough tabel die "this" model's tabel linkt naar de trough table. + +Stel je de volgende opstelleing voor: "Posts" hebben en behoren tot vele "Categories" via `posts_sections` ("Posts" have and belong to many "Categories" through `posts_sections`) + +| categories | posts_sections | posts | +|------------|------------------|---------| +| id | section_id | id | +| name | post_id | title | +| | | content | + + Class Model_Post extends ORM + { + protected $_has_many = array( + 'sections' => array( + 'model' => 'category', + 'through' => 'posts_sections', + 'far_key' => 'section_id', + ), + ); + } + + Class Model_Category extends ORM + { + protected $_has_many = array ( + 'posts' => array( + 'model' => 'post', + 'through' => 'posts_sections', + 'foreign_key' => 'section_id', + ), + ); + } + + +Uiteraard is de aliasing setup hier een beetje gek, onnodig, maar het is een goed voorbeeld om te tonen hoe het foreign/far key systeem werkt. + +### ORM Iterator + +Het is ook best te melden dat `ORM_Iterator` nu herschreven is naar `Database_Result`. + +Als je een array van ORM objecten met hun keys als index van de array wilt verkrijgen, moet je [Database_Result::as_array] gebruiken, bijvoorbeeld: + + + $objects = ORM::factory('user')->find_all()->as_array('id'); + +Waar `id` de primary key is in de user tabel. + +## Router Library + +In versie 2 was er een Router library die de main request afhandelde. Het liet je de basisroutes instellen in het `config/routes.php` bestand en het liet je toe om zelfgeschreven regex te gebruiken voor routes, maar het was niet echt flexibel als je iets radicaal wou veranderen. + +## Routes + +Het routing systeem (nu wordt verwezen naar het request systeem) is een stuk flexibeler in 3.0. Routes zijn nu gedefinieerd in het boostrap bestand (`application/bootstrap.php`) en de de module's init.php (`modules/module_name/init.php`). Het is ook interessant te weten dat routes worden geëvalueerd in de volgorde dat ze worden gedefinieerd. In plaats daarvan specifieer je een patroon voor elke uri, je kan variabelen gebruiken om segmenten aan te duiden (zoals een controller, methode, id). + +Bijvoorbeeld, in 2.x zouden deze regexes: + + $config['([a-z]+)/?(\d+)/?([a-z]*)'] = '$1/$3/$1'; + +de uri `controller/id/method` linken aan `controller/method/id`. In 3.0 gebruik je dit: + + Route::set('reversed','((/(/)))') + ->defaults(array('controller' => 'posts', 'action' => 'index')); + +[!!] Iedere uri moet een unieke naam hebben (in dit geval `reversed`), de reden hiervoor wordt nader uitgelegd in de [url tutorial](tutorials.urls). + +Slashes geven dynamische secties weer die zouden moeten worden ontleed in variabelen. Haakjes geven optionele secties aan die niet vereist zijn. Als je met een route enkel uris die beginnen met admin wilt aanspreken kan je dit gebruiken: + + Rouse::set('admin', 'admin(/(/(/)))'); + +En als je wilt een dat een gebruiker een controller specificeert: + + Route::set('admin', 'admin/(/(/))'); + +Kohana maakt geen gebruik van `default defaults`. Als je wilt dat Kohana ervan uit gaat dat de standaard actie 'index' is, dan moet je dat ook zo instellen! Dit kan je doen via [Route::defaults]. Als je zelfgeschreven regex wilt gebruiken voor uri segmenten dan moet je ene array met `segment => regex` meegeven, bijvoorbeeld: + + Route::set('reversed', '((/(/)))', array('id' => '[a-z_]+')) + ->defaults(array('controller' => 'posts', 'action' => 'index')) + +Dit zou de `id` waarde forceren om te bestaan uit kleine letters van a tot z en underscores. + +### Actions + +Nog één ding dat belangrijk is om te melden, is dat methoden in een controller die toegankelijk moeten zijn via een url nu "actions" worden genoemd. Ze krijgen een voorvoegsel 'action_'. Bijvoorbeeld in het bovenstaande voorbeeld, als de user de url `admin/posts/1/edit` aanroept dan is de actie `edit` maar is de methode die wordt aangeroepen in de controller `action_edit`. Zie de [url tutorial](tutorials.urls) voor meer informatie. + +## Sessies + +De volgende methoden worden niet meer ondersteund: Session::set_flash(), Session::keep_flash() or Session::expire_flash(), inde plaats daarvan gebruik je nu [Session::get_once]. + +## URL Helper + +Er zijn maar een aantal kleinere dingen veranderd in de url helper. `url::redirect()` werd vervangen door `$this->request->redirect()` (binnenin controllers) en `Request::instance()->redirect()`. + +`url::current` werd nu vervangen door `$this->request->uri()` + +## Valid / Validation + +Deze twee classes zijn nu samengevoegd in één enkele class met de naam `Validate`. + +De syntax om arrays te valideren is een klein beetje gewijzigd: + + $validate = new Validate($_POST); + + // Pas een filter toe op alle waarden in de array + $validate->filter(TRUE, 'trim'); + + // Om enkel rules te definiëren gebruik je rule() + $validate + ->rule('field', 'not_empty') + ->rule('field', 'matches', array('another_field')); + + // Om meerdere rules te definiëren voor een veld gebruik je rules(), je geeft een array mee met `passing an array of rules => params als tweede argument + $validate->rules('field', array( + 'not_empty' => NULL, + 'matches' => array('another_field') + )); + +De 'required' rule is ook verandert van naam. Nu wordt voor de duidelijkheid 'not_empty' gebruikt. + +## View Library + +Er zijn enkele kleine wijzigingen aangebracht aan de View library die de moeite zijn om even te melden. + +In 2.3 werden views gegenereerd binnenin de scope van de controller, dit liet je toe om `$this` te gebruiken als referentie naar de controller vanuit je view, dit is verandert in 3.0. Views worden nu gegenereerd in een lege scope. Als je nog `$this` wilt gebruiken in je view, moet je een referentie leggen via [View::bind]: `$view->bind('this', $this)`. + +Het moet wel gezegd worden dat dit een *erg* slechte manier van werken is omdat het je view koppelt aan de controller wat tegenhoud om deze view opnieuw te gebruiken. Het is aan te raden om de noodzakelijke variabelen voor je view als volgt door te sturen: + + $view = View::factory('my/view'); + + $view->variable = $this->property; + + // OF als je dit wilt "chainen" + + $view + ->set('variable', $this->property) + ->set('another_variable', 42); + + // NIET aangeraden + $view->bind('this', $this); + +Omdat de view gegenereerd wordt in een lege scope, is `Controller::_kohana_load_view` nu overtollig. Als je de view moet aanpassen vooraleer het word gegenereerd (bijvoorbeeld om een menu te gereneren over de gehele site) kan je [Controller::after] gebruiken. + + Class Controller_Hello extends Controller_Template + { + function after() + { + $this->template->menu = '...'; + + return parent::after(); + } + } diff --git a/includes/kohana/modules/userguide/guide/nl/debugging.code.md b/includes/kohana/modules/userguide/guide/nl/debugging.code.md new file mode 100644 index 00000000..6013ec72 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/debugging.code.md @@ -0,0 +1,18 @@ +# Debuggen + +Kohana heeft verschillende goede tools om je te helpen met het debuggen van je applicatie. + +De meest gebruikte is [Kohana::debug]. Deze eenvoudige methode geef alle variablen terug, vergelijkbaar met [var_export](http://php.net/var_export) of [print_r](http://php.net/print_r), maar het gebruikt HTML voor extra opmaak. + + // Toon een dump van de variabelen $foo en $bar + echo Kohana::debug($foo, $bar); + +Kohana biedt ook een methode aan om de broncode van een bepaald bestand te tonen via [Kohana::debug_source]. + + // Toon deze lijn van de broncode + echo Kohana::debug_source(__FILE__, __LINE__); + +Als je informatie wilt tonen over uw applicatie bestanden zonder te vertellen wat de installatie folder is, kan je [Kohana::debug_path] gebruiken: + + // Toont "APPPATH/cache" in plaats van het echte path + echo Kohana::debug_path(APPPATH.'cache'); diff --git a/includes/kohana/modules/userguide/guide/nl/debugging.errors.md b/includes/kohana/modules/userguide/guide/nl/debugging.errors.md new file mode 100644 index 00000000..aba56ac6 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/debugging.errors.md @@ -0,0 +1,22 @@ +# Error/Exception Handling + +Kohana biedt zowel een exception handler als een error handler aan die errors transformeert in exceptions met behulp van PHP's [ErrorException](http://php.net/errorexception) class. Veel details over de error en de interne toestand van de applicatie wordt weergegeven door de handler: + +1. Exception class +2. Error niveau +3. Error bericht +4. Bron van de error, met de errorlijn gehighlight +5. Een [debug backtrace](http://php.net/debug_backtrace) van de uitvoerings flow +6. Ingeladen bestanden, extensies en globale variablen + +## Voorbeeld + +Klik op een van de links om extra informatie te tonen: + +
    {{userguide/examples/error}}
    + +## Error/Exception Handling uitzetten + +Als je niet de interne error handling wilt gebruiken, kan je deze uitschakelen wanneer je [Kohana::init] aanroept: + + Kohana::init(array('errors' => FALSE)); diff --git a/includes/kohana/modules/userguide/guide/nl/debugging.profiling.md b/includes/kohana/modules/userguide/guide/nl/debugging.profiling.md new file mode 100644 index 00000000..9c82f7dc --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/debugging.profiling.md @@ -0,0 +1,20 @@ +# Profiling + +Kohana biedt een zeer eenvoudige manier aan om statistieken over uw aanvraag te tonen: + +1. Gewone [Kohana] methodes dat aangeroepen worden +2. Requests +3. [Database] queries +4. Gemiddelde uitvoeringstijden voor uw applicatie + +## Voorbeeld + +Je kan op elk tijdstip de huidige [profiler] statistieken tonen of opvragen: + +
    + +
    + +## Voorbeeld + +{{profiler/stats}} \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/nl/features.md b/includes/kohana/modules/userguide/guide/nl/features.md new file mode 100644 index 00000000..25cbb18e --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/features.md @@ -0,0 +1 @@ +This page lists the features of Kohana v3 \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/nl/menu.md b/includes/kohana/modules/userguide/guide/nl/menu.md new file mode 100644 index 00000000..3aa787c1 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/menu.md @@ -0,0 +1,31 @@ +1. **Ga aan de slag** + - [Wat is Kohana?](about.kohana) + - [Conventies and Codeerstijl](about.conventions) + - [Model View Controller](about.mvc) + - [Cascading Filesystem](about.filesystem) + - [Request Flow](about.flow) + - [Installatie](about.install) + - [Upgraden](about.upgrading) + - [API Browser](api) +3. **Basis gebruik** + - [Configuratie](using.configuration) + - [Laden van classes](using.autoloading) + - [Views en HTML](using.views) + - [Sessies en Cookies](using.sessions) + - [Berichten (Messages)](using.messages) +4. **Debuggen** + - [Code](debugging.code) + - [Error Handling](debugging.errors) + - [Profiling](debugging.profiling) +5. **Beveiliging** + - [XSS](security.xss) + - [Validatie](security.validation) + - [Cookies](security.cookies) + - [Database](security.database) +6. **Tutorials** + - [Hello, World](tutorials.helloworld) + - [Routes, URLs en Links](tutorials.urls) + - [URLs opschonen](tutorials.removeindex) + - [Databases](tutorials.databases) + - [ORM](tutorials.orm) + - [Werken met Git](tutorials.git) diff --git a/includes/kohana/modules/userguide/guide/nl/security.cookies.md b/includes/kohana/modules/userguide/guide/nl/security.cookies.md new file mode 100644 index 00000000..174800f6 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/security.cookies.md @@ -0,0 +1,3 @@ +# Cookie Veiligheid + +[!!] Nog niet beschikbaar diff --git a/includes/kohana/modules/userguide/guide/nl/security.database.md b/includes/kohana/modules/userguide/guide/nl/security.database.md new file mode 100644 index 00000000..aa5135b3 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/security.database.md @@ -0,0 +1,3 @@ +# Database Veiligheid + +[!!] Nog niet beschikbaar diff --git a/includes/kohana/modules/userguide/guide/nl/security.validation.md b/includes/kohana/modules/userguide/guide/nl/security.validation.md new file mode 100644 index 00000000..80947b94 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/security.validation.md @@ -0,0 +1,244 @@ +# Validatie + +Validatie kan uitgevoerd worden op elke array met behulp van de [Validate] class. Labels, filters, regels en callbacks kunnen aan het Validate object worden toegevoegd via een array key, zogenaamd een "veldnaam". + +labels +: Een label is een gebruiksvriendelijke leesbare versie van de veldnaam. + +filters +: Een filter wijzigt de waarde van een veld voordat de regels en callbacks worden uitgevoerd. + +rules +: Een regel is een controle op een veld dat "TRUE" of "FALSE" teruggeeft. A rule is a check on a field that returns `TRUE` or `FALSE`. Als een regel `FALSE` teruggeeft, wordt er een error toegevoegd aan het veld. + +callbacks +: Een callback is een zelfgeschreven methode die het gehele Validate object tot zijn beschikking heeft. De return value van een callback wordt genegeerd. In plaats daarvan moet de callback handmatig een error toevoegen aan het object met behulp van [Validate::error] wanneer een fout optreedt. + +[!!] Merk op dat de [Validate] callbacks en de [PHP callbacks](http://php.net/manual/language.pseudo-types.php#language.types.callback) niet hetzelfde zijn. + +Waneer je `TRUE` als veldnaam gebruikt bij het toevoegen van een filter, regel of callback, dan zal deze worden toegepast op alle velden met een naam. + +**Het [Validate] object zal alle velden verwijderen van de array wanneer deze niet specifiek een naam hebben gekregen via een label, filter, regel of callback. Dit voorkomt toegang tot velden die niet gevalideerd zijn als een veiligheidsmaatregel.** + +Een validatie object maken wordt gedaan door de [Validate::factory] methode: + + $post = Validate::factory($_POST); + +[!!] Het `$post` object zal worden gebruikt voor de rest van deze tutorial. Deze tutorial zal je tonen hoe je een registratie van een nieuwe gebruiker valideert. + +### Standaard regels + +Validatie heeft standaard altijd enkele regels: + +Naam van de regel | Functie +------------------------- |--------------------------------------------------- +[Validate::not_empty] | Waarde moet een niet-lege waarde zijn +[Validate::regex] | Waarde moet voldoen aan de reguliere expressie +[Validate::min_length] | Minimum aantal karakters voor een waarde +[Validate::max_length] | Maximum aantal karakters voor een waarde +[Validate::exact_length] | Waarde moet een exact aantal karakters bevatten +[Validate::email] | Een emailadres is vereist +[Validate::email_domain] | Controleer of het domein van het email bestaat +[Validate::url] | Waarde moet een URL zijn +[Validate::ip] | Waarde moet een IP address zijn +[Validate::phone] | Waarde moet een telefoonnummer zijn +[Validate::credit_card] | Waarde moet een credit card zijn +[Validate::date] | Waarde moet een datum (en tijd) zijn +[Validate::alpha] | Alleen alpha karakters toegelaten +[Validate::alpha_dash] | Alleen alpha karakters en koppeltekens toegelaten +[Validate::alpha_numeric] | Alleen alpha karakters en nummers toegelaten +[Validate::digit] | Waarde moet een geheel getal zijn +[Validate::decimal] | Waarde moet een decimaal of float getal zijn +[Validate::numeric] | Alleen nummers toegelaten +[Validate::range] | Waarde moet zich bevinden binnenin een range +[Validate::color] | Waarde moet een geldige HEX kleurencode zijn +[Validate::matches] | Waarde moet gelijk zijn aan een ander veld + +[!!] Iedere methode dat bestaat binnenin de [Validate] class kan gebruikt worden als validatie-regel zonder een volledige callback te definiëren. Bijvoorbeeld, `'not_empty'` toevoegen is hetzelfde als `array('Validate', 'not_empty')`. + +## Toevoegen van filters + +Alle validatie-filters worden gedefineerd als een veldnaam, een methode of een functie (gebruik makend van [PHP callback](http://php.net/manual/language.pseudo-types.php#language.types.callback) syntax) en een array van parameters: + + $object->filter($field, $callback, $parameter); + +Filters veranderen de waarde van een veld vooraleer deze gecontoleerd zijn via regels of callbacks. + +Indien we het veld "username" willen omvormen naar kleine letters: + + $post->filter('username', 'strtolower'); + +Als we alle witruimtes voor en na de waarde willen verwijderen voor *alle* velden: + + $post->filter(TRUE, 'trim'); + +## Toevoegen van regels + +Alle validatieregels worden gedefineerd als een veldnaam, een methode of een functie (gebruik makend van [PHP callback](http://php.net/manual/language.pseudo-types.php#language.types.callback) syntax) en een array van parameters: + + $object->rule($field, $callback, $parameter); + +Om ons voorbeeld te starten, zullen we validatie uitvoeren op een `$_POST` array die gebruikers registratie gegevens bevat: + + $post = Validate::factory($_POST); + +Vervolgens moeten we de POST-informatie met behulp van [Validate] doorlopen. Om te beginnen moeten we een aantal regels toevoegen: + + $post + ->rule('username', 'not_empty') + ->rule('username', 'regex', array('/^[a-z_.]++$/iD')) + + ->rule('password', 'not_empty') + ->rule('password', 'min_length', array('6')) + ->rule('confirm', 'matches', array('password')) + + ->rule('use_ssl', 'not_empty'); + +Iedere bestaande PHP functie kan worden gebruikt als regel. Bijvoorbeeld, als we willen controleren of een gebruiker een correcte waarde heeft ingevuld als antwoord op de SSL question: + + $post->rule('use_ssl', 'in_array', array(array('yes', 'no'))); + +Merk op dat alle array parameters steeds moeten "verpakt" worden door een array! Zonder die array, `in_array` zou worden aangeroepen als `in_array($value, 'yes', 'no')`, wat een PHP error zou teruggeven. + +Je kan eigen regels toevoegen met behulp van een [PHP callback](http://php.net/manual/language.pseudo-types.php#language.types.callback]: + + $post->rule('username', 'User_Model::unique_username'); + +[!!] Momenteel (v3.0.7) is het niet mogelijk om een object te gebruiken als rule, enkel statische methodes en functies. + +De methode `User_Model::unique_username()` zal ongeveer gedefinieerd worden als: + + public static function unique_username($username) + { + // Controleer of de username al bestaat in de database + return ! DB::select(array(DB::expr('COUNT(username)'), 'total')) + ->from('users') + ->where('username', '=', $username) + ->execute() + ->get('total'); + } + +[!!] Zelfgeschreven regels laten toe om de vele extra controles te hergebruiken voor verschillende doeleinden. Deze functies zullen meestal bestaan in een model, maar kunnen gedefinieerd worden in elke class. + +## Toevoegen van callbacks + +Alle validatie-callbacks worden gedefineerd als een veldnaam en een methode of een functie (gebruik makend van [PHP callback](http://php.net/manual/language.pseudo-types.php#language.types.callback) syntax): + + $object->callback($field, $callback); + +[!!] In tegenstelling tot filters en regels, kunnen geen parameters worden meegestuurd naar een callback. + +Het gebruikers wachtwoord moet gehashed worden indien het gevalideerd is, dus zulen we dit doen met een callback: + + $post->callback('password', array($model, 'hash_password')); + +Dit in de veronderstelling dat de `$model->hash_password()` methode er gelijkaardig uitzien als: + + public function hash_password(Validate $array, $field) + { + if ($array[$field]) + { + // Hash het wachtwoord als het bestaat + $array[$field] = sha1($array[$field]); + } + } + +# Een volledig voorbeeld + +Eerst hewwen we een [View] nodig met daarin een HTML formulier, die we plaatsen in `application/views/user/register.php`: + + + +

    Er zijn enkele fouten opgelopen, gelieve je ingevoerde gegevens opnieuw te bekijken.

    +
      + +
    • + + + +
      +
      +
      + +
      +
      +
      Wachtwoord moet minstens 6 karakters lang zijn.
      +
      +
      + +
      +
      'Altijd', 'no' => 'Enkel indien nodig'), $post['use_ssl']) ?>
      +
      Voor uw veiligheid wordt SSL altijd gebruik bij betalingen.
      +
      + + + + +[!!] Dit voorbeeld maakt veel gebruik van de [Form] helper. Het gebruik van [Form] in plaats van HTML schrijven zorgt ervoor dat alle formuliervelden overweg kunnen met ingevoerde waardes die HTML karakters bevatten. Indien je liever zelf HTML schrijft, gebruik dan zeker [HTML::chars] om gebruikersgegevens te "escapen". + +Vervolgens hebben we een controller nodig en een actie om de registratie uit te voeren, we plaatsen dit in `application/classes/controller/user.php`: + + class Controller_User extends Controller { + + public function action_register() + { + $user = Model::factory('user'); + + $post = Validate::factory($_POST) + ->filter(TRUE, 'trim') + + ->filter('username', 'strtolower') + + ->rule('username', 'not_empty') + ->rule('username', 'regex', array('/^[a-z_.]++$/iD')) + ->rule('username', array($user, 'unique_username')) + + ->rule('password', 'not_empty') + ->rule('password', 'min_length', array('6')) + ->rule('confirm', 'matches', array('password')) + + ->rule('use_ssl', 'not_empty') + ->rule('use_ssl', 'in_array', array(array('yes', 'no'))) + + ->callback('password', array($user, 'hash_password')); + + if ($post->check()) + { + // Data is gevalideerd, registreer de gebruiker + $user->register($post); + + // Altijd een redirect uitvoeren na een succesvolle POST om herladingsberichten te voorkomen. + $this->request->redirect('user/profile'); + } + + // Validatie is fout gelopen, verzamel alle errors + $errors = $post->errors('user'); + + // Toon het registratieformulier + $this->request->response = View::factory('user/register') + ->bind('post', $post) + ->bind('errors', $errors); + } + + } + +We hebben ook een user-model nodig, we plaatsen dit in `application/classes/model/user.php`: + + class Model_User extends Model { + + public function register($array) + { + // Maak een nieuw gebruikerslijn aan in de database + $id = DB::insert(array_keys($array)) + ->values($array) + ->execute(); + + // Bewaar het nieuwe gebruikers id in een cookie + cookie::set('user', $id); + + return $id; + } + + } + +Dat is het, we hebben een volledig gebruikersregistratie voorbeeld afgewerkt dat zorgvuldig ingevoerde gegevens controleert. diff --git a/includes/kohana/modules/userguide/guide/nl/security.xss.md b/includes/kohana/modules/userguide/guide/nl/security.xss.md new file mode 100644 index 00000000..05f5ed00 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/security.xss.md @@ -0,0 +1,15 @@ +# Cross-Site Scripting (XSS) Veiligheid + +De eerste stap om [XSS](http://wikipedia.org/wiki/Cross-Site_Scripting)-aanvallen te voorkomen is weten wanneer je jezelf moet beschermen. XSS kan enkel worden geactiveerd wanneer het wordt weergegeven in de HTML-inhoud, dit kan soms via een formulier-veld of worden getoond van database resultaten. Elke globale variabele dat gebruikersgegevens bevat kan worden aangetast. Dit omvat `$ _GET`, `$ _POST` en `$ _COOKIE` gegevens. + +## Het voorkomen + +Er zijn maar een paar eenvoudige regels te volgen om uw applicatie HTML te beschermen tegen XSS. De eerste stap is om de [Security::xss] methode te gebruiken om alle ingevoerde gegevens op te kuisen die afkomstig zijn van een globale variabele. Als je geen HTML wilt in een variable, gebruik dan [strip_tags](http://php.net/strip_tags) om alle ongewenste HTML tags te verwijderen van de ingevoerde waarde. + +[!!] Als je gebruikers toelaat om HTML in te voeren in je applicatie, dan is het streng aanbevolen om een HTML "opkuis-tool" te gebruiken zoals [HTML Purifier](http://htmlpurifier.org/) of [HTML Tidy](http://php.net/tidy). + +De tweede stap is om altijd de ingevoerde HTML te escapen. De [HTML] class voorziet generatoren voor veelvoorkomende tags, zo ook script en stylesheet links, ankers, afbeeldingen en e-mail (mailto) links. Elke niet-vertrouwde inhoud moet worden ge-escaped met [HTML::chars]. + +## Referenties + +* [OWASP XSS Cheat Sheet](http://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet) diff --git a/includes/kohana/modules/userguide/guide/nl/tutorials.databases.md b/includes/kohana/modules/userguide/guide/nl/tutorials.databases.md new file mode 100644 index 00000000..44f40d8e --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/tutorials.databases.md @@ -0,0 +1,248 @@ +# Databases {#top} + +Kohana 3.0 heeft een goede module ingebouwd om te kunnen werken met databases. Standaard ondersteund de database module drivers voor [MySQL](http://php.net/mysql) en [PDO](http://php.net/pdo). + +De database module zit bij de Kohana 3.0 installatie maar het moet nog worden ingesteld vooraleer je het kan gebruiken. In je `application/bootstrap.php` bestand moet je de aanroep naar [Kohana::modules] aanpassen en de database module eraan toevoegen: + + Kohana::modules(array( + ... + 'database' => MODPATH.'database', + ... + )); + +## Configuratie {#configuration} + +Nadat de module is ingesteld moet je een configuratie bestand aanmaken zodat de module weet hoe het moet connecteren met je database. Een voorbeeld configuratie bestand kan je vinden in `modules/database/config/database.php`. + +De structuur van een database configuratie groep, genoemd "instantie", ziet er als volgt uit: + + string INSTANCE_NAME => array( + 'type' => string DATABASE_TYPE, + 'connection' => array CONNECTION_ARRAY, + 'table_prefix' => string TABLE_PREFIX, + 'charset' => string CHARACTER_SET, + 'profiling' => boolean QUERY_PROFILING, + ), + +[!!] Meerdere instanties van deze instellingen kunnen worden gedefinieerd binnen het configuratie bestand. + +Het verstaan van elk van deze instellingen is belangrijk. + +INSTANCE_NAME +: Connecties kunnen elke naam hebben, maar je moet minstens één connectie hebben met de naam "default". + +DATABASE_TYPE +: Eén van de geïnstalleerde database drivers. Kohana heeft standaard de "mysql" en "pdo" drivers. + +CONNECTION_ARRAY +: Specifieke driver opties om te connecteren naar je database. (Driver opties worden uitgelegd [beneden](#connection_settings).) + +TABLE_PREFIX +: Voorvoegsel dat wordt toegevoegd aan al je tabelnamen door de [query builder](#query_building). + +QUERY_PROFILING +: Zet [profiling](debugging.profiling) aan van database queries. + +### Voorbeeld + +Het voorbeeld bestand hieronder toont 2 MySQL connecties, een lokale en één op afstand (=remote). + + return array + ( + 'default' => array + ( + 'type' => 'mysql', + 'connection' => array( + 'hostname' => 'localhost', + 'username' => 'dbuser', + 'password' => 'mijnwachtwoord', + 'persistent' => FALSE, + 'database' => 'mijn_db_naam', + ), + 'table_prefix' => '', + 'charset' => 'utf8', + 'profiling' => TRUE, + ), + 'remote' => array( + 'type' => 'mysql', + 'connection' => array( + 'hostname' => '55.55.55.55', + 'username' => 'remote_user', + 'password' => 'mijnwachtwoord', + 'persistent' => FALSE, + 'database' => 'mijn_remote_db_naam', + ), + 'table_prefix' => '', + 'charset' => 'utf8', + 'profiling' => TRUE, + ), + ); + +### Connectie instellingen {#connection_settings} + +Iedere database driver heeft verschillende connectie instellingen. + +#### MySQL + +Een MySQL database accepteert de volgende opties in de `connection` array: + +Type | Optie | Omschrijving | Standaard Waarde +----------|------------|----------------------------| ------------------------- +`string` | hostname | Hostname van de database | `localhost` +`integer` | port | Poort nummer | `NULL` +`string` | socket | UNIX socket | `NULL` +`string` | username | Database gebruikersnaam | `NULL` +`string` | password | Database wachtwoord | `NULL` +`boolean` | persistent | Persistente connecties | `FALSE` +`string` | database | Database naam | `kohana` + +#### PDO + +Een PDO database accepteert de volgende opties in de `connection` array: + +Type | Optie | Omschrijving | Standaard Waarde +----------|------------|----------------------------| ------------------------- +`string` | dsn | PDO data source identifier | `localhost` +`string` | username | Database gebruikersnaam | `NULL` +`string` | password | Database wachtwoord | `NULL` +`boolean` | persistent | Persistente connecties | `FALSE` + +[!!] Als je PDO gebruikt en je bent niet zeker wat je moet gebruiken voor de `dsn` optie, bekijk dan [PDO::__construct](http://php.net/pdo.construct). + +## Connecties en Instanties {#connections} + +Iedere configuratie groep verwijst naar een database instantie. Iedere instantie kan worden aangesproken via [Database::instance]: + + $default = Database::instance(); + $remote = Database::instance('remote'); + +Om de database los te koppelen, moet je gewoonweg het object vernietigen: + + unset($default, Database::$instances['default']); + +Om all database instanties in één keer los te koppelen, gebruik je: + + Database::$instances = array(); + +## Het maken van Queries {#making_queries} + +Er zijn twee verschillende manieren om queries te maken. De eenvoudigste manier om een query te maken is het gebruik van [Database_Query], via [DB::query]. Deze queries worden "prepared statements" genoemd en laat je toe om query parameters instellen die automatisch worden "geescaped". De tweede manier om een query te maken is door deze op te bouwen met behulp van methode-aanroepen. Dit wordt gedaan met behulp van de [query builder](#query_building). + +[!!] Alle queries worden uitgevoerd via de `execute` methode, deze verwacht een [Database] object of een instantienaam. Zie [Database_Query::execute] voor meer informatie. + +### Prepared Statements + +Het gebruik van prepared statements laat je toe om SQL queries manueel te schrijven terwijl de query waarden nog steeds automatisch worden "geescaped" om [SQL injectie](http://wikipedia.org/wiki/SQL_Injection) te voorkomen. Een query aanmaken is relatief gemakkelijk: + + $query = DB::query(Database::SELECT, 'SELECT * FROM users WHERE username = :user'); + +De [DB::query] factory methode creëert een nieuwe [Database_Query] class voor ons, zodat "methode-chaining" mogelijk is. De query bevat een `:user` parameter, die we kunnen toewijzen aan een waarde: + + $query->param(':user', 'john'); + +[!!] Parameter namen kunnen elke string zijn aangezien worden vervangen via het gebruik van [strtr](http://php.net/strtr). Het wordt ten zeerste aanbevolen om **geen** dollar tekens te gebruiken als parameter namen om verwarring te voorkomen. + +Als je de SQL wilt tonen dat zal worden uitgevoerd, moet je het object gewoonweg casten naar een string: + + echo Kohana::debug((string) $query); + // Zou moeten tonen: + // SELECT * FROM users WHERE username = 'john' + +Je kan ook altijd de `:user` parameter aanpassen door de [Database_Query::param] opnieuw aan te roepen: + + $query->param(':user', $_GET['search']); + +[!!] Indien je meerdere paramters in één keer wilt instellen kan je dat doen met [Database_Query::parameters]. + +Eénmaal je iets hebt toegewezen aan elke parameter, kan je de query uitvoeren: + + $query->execute(); + +Het is ook mogelijk om een parameter te "verbinden" met een variabele, door het gebruik van een [variabele referentie]((http://php.net/language.references.whatdo)). Dit kan extreem gebruikvol zijn wanneer je dezelfde query meerdere keren moet uitvoeren: + + $query = DB::query(Database::INSERT, 'INSERT INTO users (username, password) VALUES (:user, :pass)') + ->bind(':user', $username) + ->bind(':pass', $password); + + foreach ($new_users as $username => $password) + { + $query->execute(); + } + +In het bovenstaand voorbeeld worden de variabelen `$username` en `$password` gewijzigd in iedere loop van het `foreach` statement. Wanneer de parameter verandert, veranderen infeite de `:user` en `:pass` query parameters. Het zorgvuldig gebruik van parameter binding kan een pak code besparen. + +### Query Building {#query_building} + +Het maken van dynamische queries via objecten en methodes zorgt ervoor dat queries zeer snel kunnen worden geschreven op een agnostische manier. Query building voegt ook identifier (tabel en kolom naam) en value quoting toe. + +[!!] Op dit moment, is het niet mogelijk om query building te combineren met prepared statements. + +#### SELECT + +Elk type database query wordt vertegenwoordigd door een andere class, elk met hun eigen methoden. Bijvoorbeeld, om een SELECT-query te maken, gebruiken we [DB::select]: + + $query = DB::select()->from('users')->where('username', '=', 'john'); + +Standaard zal [DB::select] alle kolommen selecteren (`SELECT * ...`), maar je kan ook specificeren welke kolommen je wilt teruggeven: + + $query = DB::select('username', 'password')->from('users')->where('username', '=', 'john'); + +Neem nu een minuut de tijd om te kijken wat deze methode-keten doet. Eerst maken we een selectie object met behulp van [DB::select]. Vervolgens stellen we tabel(len) in door de `from` methode te gebruiken. Als laatste stap zoeken we voor specifieke records door gebruik te maken van de `where` methode. We kunnen de SQL tonen dat zal worden uitgevoerd door deze te casten naar een string: + + echo Kohana::debug((string) $query); + // Zou moeten tonen: + // SELECT `username`, `password` FROM `users` WHERE `username` = 'john' + +Merk op hoe de kolom en tabel namen automatisch worden "geescaped", eveneens de waarden? Dit is een van de belangrijkste voordelen van het gebruik van de query builder. + +Het is mogelijk om `AS` aliassen te maken wanneer je iets selecteert: + + $query = DB::select(array('username', 'u'), array('password', 'p'))->from('users'); + +Deze query zal de volgende SQL genereren: + + SELECT `username` AS `u`, `password` AS `p` FROM `users` + +#### INSERT + +Om records aan te maken in de database gebruik je [DB::insert] om een INSERT query aan te maken: + + $query = DB::insert('users', array('username', 'password'))->values(array('fred', 'p@5sW0Rd')); + +Deze query zal de volgende SQL genereren: + + INSERT INTO `users` (`username`, `password`) VALUES ('fred', 'p@5sW0Rd') + +#### UPDATE + +Om een bestaande record aan te passen gebruik je [DB::update] om een UPDATE query aan te maken: + + $query = DB::update('users')->set(array('username' => 'jane'))->where('username', '=', 'john'); + +Deze query zal de volgende SQL genereren: + + UPDATE `users` SET `username` = 'jane' WHERE `username` = 'john' + +#### DELETE + +Om een bestaande record te verwijderen gebruik je [DB::delete] om een DELETE query aan te maken: + + $query = DB::delete('users')->where('username', 'IN', array('john', 'jane')); + +Deze query zal de volgende SQL genereren: + + DELETE FROM `users` WHERE `username` IN ('john', 'jane') + +#### Database Functies {#database_functions} + +Uiteindelijk zal je waarschijnlijk uitdraaien in een situatie waar je beroep moet doen op `COUNT` of een andere database functie binnenin je query. De query builder ondersteunt deze functies op twee manieren. De eerste mogelijkheid is met behulp van aanhalingstekens binnenin de aliassen: + + $query = DB::select(array('COUNT("username")', 'total_users'))->from('users'); + +Dit ziet er bijna precies hetzelfde uit als een standaard "AS" alias, maar let op hoe de kolom naam is verpakt in dubbele aanhalingstekens. Iedere keer als er een waarde met dubbele aanhalingstekens verschijnt binnenin een kolom naam, wordt **alleen** het gedeelte binnen de dubbele aanhalingstekens "geescaped". Deze query zal de volgende SQL genereren: + + SELECT COUNT(`username`) AS `total_users` FROM `users` + +#### Complexe Expressies + +Aliassen met aanhalingstekens zullen de meeste problemen oplossen, maar van tijd tot tijd kan je in een situatie komen waar je een complexe expressie kunt gebruiken. In deze gevallen moet je een database expressie gebruiken die je kan creëren met [DB::expr]. Een database expressie wordt als directe input genomen en er wordt geen "escaping" uitgevoerd. diff --git a/includes/kohana/modules/userguide/guide/nl/tutorials.git.md b/includes/kohana/modules/userguide/guide/nl/tutorials.git.md new file mode 100644 index 00000000..0595654b --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/tutorials.git.md @@ -0,0 +1,149 @@ +# Werken met Git + +Kohana gebruikt [git](http://git-scm.com/) als versie controle systeem en [github](http://github.com/kohana) voor community-bijdragen. Deze tutorial zal je tonen hoe je beide platformen kunt gebruiken om een applicatie op te zetten. + +## Het installeren en instellen van Git op uw computer + +### Installeren van Git + +- OSX: [Git-OSX](http://code.google.com/p/git-osx-installer/) +- Windows: [Msygit](http://code.google.com/p/msysgit/) +- Of download het van de [git-site](http://git-scm.com/) en installeer het manueel (zie de git website) + +### Basis globale instellingen + + git config --global user.name "Uw Naam" + git config --global user.email "uwemail@website.com" + +### Extra, maar aan te raden instellingen + +Om een beter visueel overzicht te hebben van de git commando's en repositories in je console stel je best volgende in: + + git config --global color.diff auto + git config --global color.status auto + git config --global color.branch auto + +### Automatische aanvulling installeren + +[!!] Deze lijnen code zijn enkel van toepassing voor OSX + +Deze lijnen code doen al het vuile werk voor je zodat automatische aanvulling kan werken voor uw git-omgeving + + cd /tmp + git clone git://git.kernel.org/pub/scm/git/git.git + cd git + git checkout v`git --version | awk '{print $3}'` + cp contrib/completion/git-completion.bash ~/.git-completion.bash + cd ~ + rm -rf /tmp/git + echo -e "source ~/.git-completion.bash" >> .profile + +### Gebruik altijd LF als regeleinden + +Dit is de conventie die we maken met Kohana. Stel deze instellingen voor uw eigen goed en vooral als je wilt bijdragen aan de Kohana community. + + git config --global core.autocrlf input + git config --global core.savecrlf true + +[!!] Meer informatie over regeleinden kan je vinden op [github](http://help.github.com/dealing-with-lineendings/) + +### Meer informatie op je op weg te zetten + +- [Git Screencasts](http://www.gitcasts.com/) +- [Git Reference](http://gitref.org/) +- [Pro Git book](http://progit.org/book/) + +## Initile structuur + +[!!] Deze tutorial zal ervan uitgaan dat uw webserver al is ingesteld, en dat je een nieuwe applicatie zal maken op . + +Met behulp van je console, ga naar de lege map `gitorial` en voer `git init` uit. Dit zal een ruwe structuur voor een nieuwe git repository aanmaken. + +Vervolgend zullen we een [submodule](http://www.kernel.org/pub/software/scm/git/docs/git-submodule.html) maken voor de `system` folder. Ga naar en kopieer de "Clone URL": + +![Github Clone URL](http://img.skitch.com/20091019-rud5mmqbf776jwua6hx9nm1n.png) + +Gebruik nu de URL om de submodule aan te maken voor `system`: + + git submodule add git://github.com/kohana/core.git system + +[!!] Dit creert een link naar de huidige ontwikkelingsversie voor de volgende stabiele uitgave. De ontwikkelingsversie is meestal veilig om te gebruiken, het heeft dezelfde API als de huidige stabiele download maar met bugfixes al toegepast. + +Voeg nu elke submodule toe dat je wil. Bijvoorbeeld als je de [Database] module nodig hebt: + + git submodule add git://github.com/kohana/database.git modules/database + +Nadat de submodules zijn toegevoegd, moet je ze nog initialiseren: + + git submodule init + +Nu dat de submodules zijn toegevoegd en geinitialiseerd, kan je ze commit'en: + + git commit -m 'Added initial submodules' + +Vervolgens creren we de applicatie folder structuur. Hier is een absoluut minimum vereist: + + mkdir -p application/classes/{controller,model} + mkdir -p application/{config,views} + mkdir -m 0777 -p application/{cache,logs} + +Als je nu `find application` uitvoert, moet je dit zien: + + application + application/cache + application/config + application/classes + application/classes/controller + application/classes/model + application/logs + application/views + +We willen niet dat git de log of cache bestanden volgt dus voegen we een `.gitignore` bestand toe aan deze folders. Dit zal alle niet-verborgen bestanden negeren: + + echo '[^.]*' > application/{logs,cache}/.gitignore + +[!!] Git negeert lege folders, dus het toevoegen van een `.gitignore` bestand zorgt er voor dat git de folder volgt maar niet de bestanden er in. + +Nu hebben we nog de `index.php` en `bootstrap.php` bestanden nodig: + + wget http://github.com/kohana/kohana/raw/master/index.php + wget http://github.com/kohana/kohana/raw/master/application/bootstrap.php -O application/bootstrap.php + +Commit deze veranderingen ook: + + git add application + git commit -m 'Added initial directory structure' + +Dit is alles wat je nodig hebt. Je hebt nu een applicatie dat Git gebruikt als versiesysteem. + +## Updaten van Submodules + +Op een gegeven moment zal je waarschijnlijk ook je submodules willen upgraden. Om al je submodules te updaten naar de laatste "HEAD" versie: + + git submodule foreach 'git checkout master && git pull origin master' + +Om een enkele submodule te update, bijvoorbeel `system`: + + cd system + git checkout master + git pull origin master + cd .. + git add system + git commit -m 'Updated system to latest version' + +Als je een enkele submodule wilt updaten naar een specifieke commit: + + cd modules/database + git pull origin master + git checkout fbfdea919028b951c23c3d99d2bc1f5bbeda0c0b + cd ../.. + git add database + git commit -m 'Updated database module' + +Merk op dat je ook een commit kunt uitchecken via een tag, zoals een officieel versie, bijvoorbeeld: + + git checkout 3.0.6 + +Voer gewoon `git tag` uit zonder parameters om een lijst van alle tags te krijgen. + +U weet nu "alles" over git! diff --git a/includes/kohana/modules/userguide/guide/nl/tutorials.helloworld.md b/includes/kohana/modules/userguide/guide/nl/tutorials.helloworld.md new file mode 100644 index 00000000..7392571b --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/tutorials.helloworld.md @@ -0,0 +1,106 @@ +# Hello, World + +Aangezien bijna ieder framework een soort van "hello world" voorbeeld heeft, zou het onbeleefd van ons zijn om deze traditie te doorbreken! + +We gaan starten met het maken van een zeer basis hello world, om vervolgens uit te breiden om het MVC principe te volgen. + +## Tot op het bot + +Eerst moeten we een controller maken dat Kohana kan gebruiken om de request af te handelen. + +Maak het bestand `application/classes/controller/hello.php` in uw applicatie folder en zorg voor deze code erin: + + template->message = 'hello, world!'; + } + } + +`extends Controller_Template` +: We breiden nu uit van de template controller, dit maakt het meer logisch om views te gebruiken in onze controller. + +`public $template = 'site';` +: De template controller moet weten welke template we willen gebruiken. Het zal automatisch de view inladen die gedefinieerd is in deze variabele en het view object eraan toewijzen. + +`$this->template->message = 'hello, world!';` +: `$this->template` is een referentie naar het view object voor onze site template. Wat we hier doen is een variabele "message", met waarde "hello, world", toewijzen aan de view. + +Laten we nu proberen onze code uit te voeren... + +
      {{userguide/examples/hello_world_error}}
      + +Voor de één of andere reden geeft Kohana een error en toont het niet ons cool bericht. + +Als we kijken naar het error-bericht kunnen we zien dat de View library onze site template niet kon vinden, waarschijnlijk omdat we er nog geen aangemaakt hebben - *doh*! + +Laten we het view bestand `application/views/site.php` aanmaken voor ons bericht: + + + + We've got a message for you! + + + +

      +

      We just wanted to say it! :)

      + + + +Als we de pagina vernieuwen dan kunnen we de vruchten zien van ons *zwaar" werk: + +![hello, world! We just wanted to say it!](img/hello_world_2.png "hello, world! We just wanted to say it!") + +## Stage 3 – Profit! + +In deze tutorial heb je geleerd hoe je een controller maakt en een view gebruikt om je logica te scheiden van het visuele. + +Dit is natuurlijk een zeer elementaire inleiding over het werken met Kohana en toont zelfs niet de sterkte van het framework voor wanneer je applicaties hiermee ontwikkelt. diff --git a/includes/kohana/modules/userguide/guide/nl/tutorials.orm.md b/includes/kohana/modules/userguide/guide/nl/tutorials.orm.md new file mode 100644 index 00000000..bc5be9f2 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/tutorials.orm.md @@ -0,0 +1,298 @@ +# ORM {#top} + +Kohana 3.0 bevat een krachtige ORM-module die het "active record"-patroon gebruikt en database introspectie gebruikt om kolominformatie te bepalen van een model. + +De ORM-module is opgenomen in de Kohana 3.0 installatie maar moet worden ingeschakeld vooraleer je het kunt gebruiken. In je `application/bootstrap.php` bestand moet je de oproen naar [Kohana::modules] aanpassen en de ORM-module insluiten: + + Kohana::modules(array( + ... + 'orm' => MODPATH.'orm', + ... + )); + +## Configuratie {#configuration} + +ORM vergt weinig configuratie om aan de slag te kunnen. Breid uw model classes uit met ORM om de module te kunnen gebruiken: + + class Model_User extends ORM + { + ... + } + +In het voorbeeld hierboven zal het model zoeken naar een tabel `users` in de standaard database. + +### Model Configuratie Properties + +De volgende eigenschappen worden gebruikt om ieder model te configureren: + +Type | Eigenschap | Omschrijving | Standaard waarde +----------|---------------------|--------------------------------------| ------------------------- +`string` | _table_name | Tabelnaam om te gebruiken | `singular model name` +`string` | _db | Naam van de database om te gebruiken | `default` +`string` | _primary_key | Kolom die dient als primary key | `id` +`string` | _primary_val | Kolom die dient als primary value | `name` +`bool` | _table_names_plural | Zijn de tabelnamen meervoudig? | `TRUE` +`array` | _sorting | Array met kolom => volgorde | `primary key => ASC` +`string` | _foreign_key_suffix | Achtervoegsel voor foreign keys | `_id` + +## Het gebruik van ORM + +### Een Record inladen + +Om een instantie van een model aan te maken, kan je de [ORM::factory] methode of [ORM::__construct] gebruiken: + + $user = ORM::factory('user'); + // of + $user = new Model_User(); + +De constructor en factory methodes accepteren ook een primary key waarde om het gegeven model's data in te laden: + + // Laad gebruiker met ID 5 + $user = ORM::factory('user', 5); + + // Kijk of de gebruiker succesvol werd ingeladen + if ($user->loaded()) { ... } + +Je kan optioneel een array met keys => value paren meegeven om een data object in te laden die voldoet aan de gegeven criteria: + + // Laad een gebruiker met email joe@example.com + $user = ORM::factory('user', array('email' => 'joe@example.com')); + +### Zoeken naar één of meerdere records + +ORM ondersteunt de meeste krachtige [Database] methoden voor het doorzoeken van gegevens van uw model. Zie de `_db_methods` eigenschap voor een volledige lijst van ondersteunde methode oproepen. Records worden opgehaald met behulp van de [ORM::find] en [ORM::find_all] functies. + + // Dit zal de eerste actieve gebruiker nemen met de naam Bob + $user = ORM::factory('user') + ->where('active', '=', TRUE) + ->where('name', '=', 'Bob') + ->find(); + + // Dit zal alle gebruikers nemen met de naam Bob + $users = ORM::factory('user') + ->where('name', '=', 'Bob') + ->find_all(); + +Wanneer je een lijst van modellen ontvangt met behulp van [ORM::find_all], kan je deze doorlopen zoals je doet met database resultaten: + + foreach ($users as $user) + { + ... + } + +Een zeer handige functie van ORM is de [ORM::as_array] methode die het record zal teruggeven als een array. Indien je dit gebruikt met [ORM::find_all], zal een array van alle records worden teruggegeven. Een goed voorbeeld van wanneer dit nuttig is, is voor select in het HTML formulier: + + // Toon een dropdown/select met daarin alle gebruikersnamen (id als value van de options) + echo Form::select('user', ORM::factory('user')->find_all()->as_array('id', 'username')); + +### Het aantal records tellen + +Gebruik [ORM::count_all] om het aantal records terug te geven voor een bepaalde query. + + // Aantal actieve gebruikers + $count = ORM::factory('user')->where('active', '=', TRUE)->count_all(); + +Als je het totaal aantal gebruikers wilt tellen voor een bepaalde query, terwijl je enkel een bepaalde set van deze gebruikers wilt tonen, gebruik dan de [ORM::reset] methode met `FALSE` vooraleer je `count_all` gebruikt: + + $user = ORM::factory('user'); + + // Totaal aantal gebruikers (reset FALSE zorgt ervoor dat het query object dat het query object niet geleegd wordt) + $count = $user->where('active', '=', TRUE)->reset(FALSE)->count_all(); + + // Geef enkel de eerste 10 resultaten terug van deze resultaten + $users = $user->limit(10)->find_all(); + +### Properties van een model aanspreken + +Alle model properties zijn toegankelijk via de `__get` en `__set` magic methodes. + + $user = ORM::factory('user', 5); + + // Geef de gebruikersnaam terug + echo $user->name; + + // Verander de gebruiker zijn naam + $user->name = 'Bob'; + +Voor het opslaan van gegevens/properties die niet bestaan in de tabel van het model, kan je gebruik maken van het `_ignored_columns` data member. De gegevens zullen worden opgeslagen in het interne `_object` member, maar zal worden genegeerd op database-niveau. + + class Model_User extends ORM + { + ... + protected $_ignored_columns = array('field1', 'field2', …); + ... + } + +Meerdere key => value paren kunnen worden ingesteld door gebruik te maken van de [ORM::values] methode. + + $user->values(array('username' => 'Joe', 'password' => 'bob')); + +### Aanmaken en opslaan van records + +De methode [ORM::save] wordt gebruikt om zowel nieuwe records aan te maken als het upaten van bestaande. + + // Nieuw record aanmaken + $user = ORM::factory('user'); + $user->name = 'Nieuwe gebruiker'; + $user->save(); + + // Aanpassen van een bestaand record + $user = ORM::factory('user', 5); + $user->name = 'Gebruiker 2'; + $user->save(); + + // Controleer of het record opgeslagen is + if ($user->saved()) { ... } + +Je kan meerdere records tegelijk veranderen met de [ORM::save_all] methode: + + $user = ORM::factory('user'); + $user->name = 'Bob'; + + // Verander bij alle actieve gebruikers de naam naar 'Bob' + $user->where('active', '=', TRUE)->save_all(); + +#### Gebruik `Updated` en `Created` kolommen + +De `_updated_column` en `_created_column` members staan ter beschikking om automatisch aangepast te worden wanneer een model wordt gecreëerd of aangepast. Ze worden standaard niet gebruikt. Om ze te gebruiken: + + // date_created is de kolom die wordt gebruikt om de aanmaak datum op te slaan. Gebruik format => TRUE om een timestamp op te slaan + protected $_created_column = array('date_created', 'format' => TRUE); + + // date_modified is de kolom die wordt gebruikt om de datum op te slaan wanneer het item is aangepast. In dit geval wordt een string gebruikt om een date() formaat te specificeren + protected $_updated_column = array('date_modified', 'format' => 'm/d/Y'); + +### Verwijderen van records + +Records worden verwijderd met [ORM::delete] en [ORM::delete_all]. Deze methoden werken op dezelfde manier als het opslaan van records zoals hierboven beschreven, met de uitzondering dat [ORM::delete] nog een optionele parameter heeft, het `id` van het record om te verwijderen. Anders wordt het huidig ingeladen record verwijderd. + +### Relaties + +ORM ondersteunt zeer goed relateies. Ruby heeft een [goede tutorial omtrent relaties](http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html). + +#### Belongs-To en Has-Many + +We gaan er van uit dat we werken met een school dat veel (has many) studenten heeft. Iedere student kan enkel maar tot één school behoren (belong to). Dan zullen de relaties als volgt gedefinieerd worden: + + // In het model "school" + protected $_has_many = array('students' => array()); + + // In het model "student" + protected $_belongs_to = array('school' => array()); + +Om een student zijn school te verkrijgen gebruik je: + + $school = $student->school; + +Om een school zijn studenten te verkrijgen gebruik je: + + // Merk op dat find_all is vereist na "students" + $students = $school->students->find_all(); + + // Om resultaten te "filteren": + $students = $school->students->where('active', '=', TRUE)->find_all(); + +Standaard zal ORM willen zoeken naar een `school_id` model in de studenten tabel. Dit kan worden overschreven door gebruik te maken van het `foreign_key` attribuut: + + protected $_belongs_to = array('school' => array('foreign_key' => 'schoolID')); + +De foreign key moet overschreven worden in zowel het student als school model. + +#### Has-One + +Has-One is een speciale versie van Has-Many, het enige verschil is dat er maar één enkel record is. In het bovenstaande voorbeeld zou iedere school maar één student hebben (al is dat wel een slecht voorbeeld). + + // In het model "school" + protected $_has_one = array('student' => array()); + +Je moet niet zoals bij Belongs-To de `find` methode gebruiken wanneer je verwijst naar een het Has-One gerelateerd object, dit gebeurt automatisch. + +#### Has-Many "Through" + +De Has-Many "through" relatie (ook bekend als Has-And-Belongs-To-Many) wordt gebruikt in het geval dat één object gerelateerd is met meerdere objecten van verschillende types en omgekeerd. Bijvoorbeeld, een student kan verschillende klassen volgen en een klass kan verschillende studenten hebben. In dit geval wordt een derde tabel gebruikt en een model die dienst doet als `pivot`. In dit geval noemen we het pivot object/model `enrollment` (=inschrijving). + + // In het model "student" + protected $_has_many = array('classes' => array('through' => 'enrollment')); + + // In het model "class" + protected $_has_many = array('students' => array('through' => 'enrollment')); + +De inschrijvingstabel (`enrollment`) moet twee foreign keys hebben, een voor `class_id` en de andere voor `student_id`. Deze kunnen worden overschreven door `foreign_key` en `far_key` te gebruiken bij het definiëren van de relatie. Bijvoorbeeld: + + // In het model "student" (de foreign key verwijst naar dit model [student], terwijl de far key verwijst naar het andere model [class]) + protected $_has_many = array('classes' => array('through' => 'enrollment', 'foreign_key' => 'studentID', 'far_key' => 'classID')); + + // In het model "class" + protected $_has_many = array('students' => array('through' => 'enrollment', 'foreign_key' => 'classID', 'far_key' => 'studentID')); + +Het inschrijvings model (enrollment) zal als volgt gedefinieerd worden: + + // Het model "enrollment" hoort bij zowel "student" als "class" + protected $_belongs_to = array('student' => array(), 'class' => array()); + +Om de gerelateerde objecten te bereiken, gebruik je: + + // Om de klassen van een student te verkrijgen + $student->classes->find_all(); + + // Om studenten te verkrijven vanuit de klas + $class->students->find_all(); + +### Validatie + +ORM werkt nauw samen met de [Validate] library. ORM biedt de volgende members aan voor validatie + +* _rules +* _callbacks +* _filters +* _labels + +#### `_rules` + + protected $_rules = array + ( + 'username' => array('not_empty' => array()), + 'email' => array('not_empty' => array(), 'email' => array()), + ); + +`username` zal gecontroleerd worden om zeker niet leeg te zijn. `email` zal ook gecontroleerd worden om te verzekeren dat het een geldig emailadres is. De lege arrays die als values worden meegestuurd, kunnen worden gebruikt om optionele parameters mee te geven aan deze functie aanroepen. + +#### `_callbacks` + + protected $_callbacks = array + ( + 'username' => array('username_unique'), + ); + +`username` zal worden meegestuurd naar een callback methode `username_unique`. Als de methode bestaat in het huidige model, zal het worden gebruikt, anders zal een globale functie worden opgeroepen. Hier is een voorbeeld van z'n methode: + + public function username_unique(Validate $data, $field) + { + // Logica om te controleren of de gebruikersnaam uniek is + ... + } + +#### `_filters` + + protected $_filters = array + ( + TRUE => array('trim' => array()), + 'username' => array('stripslashes' => array()), + ); + +`TRUE` slaat erop dat de `trim` filter wordt gebruikt voor alle velden. `username` zal ook gefilterd worden door `stripslashes` vooraleer het gevalideerd wordt. De lege arrays die als values worden meegestuurd, kunnen worden gebruikt om optionele parameters mee te geven aan deze filter-functie aanroepen. + +#### Controleren of een Object Valid is + +Gebruik [ORM::check] om te kijken of het object momenteel valid is. + + // Een object zijn values instellen en dan controleren of het valid is + if ($user->values($_POST)->check()) + { + $user->save(); + } + +Je kan de `validate()` methode gebruiken om een model zijn validatie object aan te roepen. + + // Een optionele filter manueel toevoegen + $user->validate()->filter('username', 'trim'); \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/nl/tutorials.removeindex.md b/includes/kohana/modules/userguide/guide/nl/tutorials.removeindex.md new file mode 100644 index 00000000..dc8864c5 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/tutorials.removeindex.md @@ -0,0 +1,88 @@ +# `index.php` verwijderen uit de URL + +Om uw URLs proper te houden, wil je hoogtswaarschijnlijk je applicatie kunnen benaderen zonder /index.php/` in uw URL te gebruiken. Er zijn twee stappen om `index.php` te verwijderen uit de URL. + +1. Het bootstrap bestand aanpassen +2. Herschrijven van URL's instellen + +# Configuratie van de Bootstrap + +Het eerste dat je moet veranderen is de `index_file` instelling van [Kohana::init]: + + Kohana::init(array( + 'base_url' => '/myapp/', + 'index_file' => FALSE, + )); + +Nu zullen alle links die gegeneerd worden met [URL::site], [URL::base] en [HTML::anchor] niet meer "index.php" gebruiken in de URL. Alle gegenereerde links zullen starten met `/myapp/` in plaats van `/myapp/index.php/`. + +# URL Herschrijven + +Het herschrijven van URL kan verschillen, naargelang je web server. + +## Apache + +Hernoem `example.htaccess` naar `.htaccess` en verander de volgende regel code: + + RewriteBase /kohana/ + +Dit moet gelijk zijn met de `base_url` instelling van [Kohana::init]: + + RewriteBase /myapp/ + +In de meeste gevallen is dit het enige dat je moet veranderen. + +### Er loopt iets fout! + +Als je een "Internal Server Error" of "No input file specified" error krijgt, probeer dan hetvolgende te veranderen: + + RewriteRule ^(?:application|modules|system)\b - [F,L] + +Door enkel een slash te gebruiken: + + RewriteRule ^(application|modules|system)/ - [F,L] + +Als het nog steeds niet werkt, probeer dan hetvolgende te veranderen: + + RewriteRule .* index.php/$0 [PT] + +Naar iets simpeler: + + RewriteRule .* index.php [PT] + +### Nog steeds niet loopt het fout! + +Als je nog steeds fouten krijgt, controleer dan zeker dat je host wel URL `mod_rewrite` ondersteund. Als je de Apache configuratie kunt aanpassen, voeg dan deze lijnen toe aan de configuratie, meestal in `httpd.conf`: + + + Order allow,deny + Allow from all + AllowOverride All + + +## NGINX + +Het is moeilijk om voorbeelden te geven van een nginx configuratie, maar hier is een voorbeeld voor een server: + + location / { + index index.php index.html index.htm; + try_files $uri $uri/ index.php$uri?$args; + } + + location ~ ^(.+\.php)(.*)$ { + fastcgi_split_path_info ^(.+\.php)(.*)$; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + + include fastcgi.conf; + + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + } + +Er zijn twee dingen te melden: het gebruik van [try_files](http://wiki.nginx.org/NginxHttpCoreModule#try_files) en [fastcgi_split_path_info](http://wiki.nginx.org/NginxHttpFcgiModule#fastcgi_split_path_info). + +[!!] Dit in de veronderstelling dat je PHP draait als een FastCGI server op poort 9000 en dat je nginx v0.7.31 of later gebruikt. + +Als je problemen hebt om dit te laten werken, zet dan het deub level logging aan in nginx en controleer de toegangs- en foutenlogs. diff --git a/includes/kohana/modules/userguide/guide/nl/tutorials.urls.md b/includes/kohana/modules/userguide/guide/nl/tutorials.urls.md new file mode 100644 index 00000000..e13ea751 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/tutorials.urls.md @@ -0,0 +1,159 @@ +# Routes, URLs en Links + +Dit onderdeel zal je een basis idee geven achter Kohana's request routing, de generatie van url's en links. + +## Routing + +Zoals gezegd in de [Request Flow](about.flow) sectie, wordt een request afgehandeld door de [Request] class die een juiste [Route] vindt en de juiste controller inlaadt om het request af te handelen. Dit systeem biedt veel flexibiliteit en een logische manier van werken. + +Als je kijkt in `APPPATH/bootstrap.php` zal je zien dat de volgende code onmiddelijk wordt aangeroepen vooraleer de request wordt toegewezen aan [Request::instance]: + + Route::set('default', '((/(/)))') + ->defaults(array( + 'controller' => 'welcome', + 'action' => 'index', + )); + +Dit stelt de `default` route in voor een uri met het formaat `((/(/)))`. De karakters omringd met `<>` zijn *keys* en de karakters omringd met `()` zijn optionele onderdelen van de uri. In dit geval is de gehele uri optioneel, zodat bij een lege uri de standaard controller en actie worden uitgevoerd wat ervoor zou zorgen dat de `Controller_Welcome` class wordt ingeladen en eventueel wordt de methode `action_index` aangeroepen om de request af te handelen. + +Merk op dat in Kohana routes, alle karakters zijn toegestaan behalve `()<>` en de `/`, die hebben namelijk een speciale betekenis. In de standaard route wordt de "/" gebruikt als scheidingsteken, maar zolang de reguliere expressie logisch en doordacht is, kan je kiezen hoe je routes er laat uitzien. + +### Folders + +Om je controllers wat meer te gaan organiseren kan je ervoor kiezen om ze te plaatsen in subfolders. Een veel voorkomend geval is voor een backend van je website: + + Route::set('admin', 'admin(/(/(/)))') + ->defaults(array( + 'directory' => 'admin', + 'controller' => 'home', + 'action' => 'index', + )); + +Deze route vereist dat de uri moet beginnen met `admin` en dat de folder statisch wordt toegewezen aan `admin` in de standaard instellingen van de route. Een request naar `admin/users/create` zal nu de `Controller_Admin_Users` class laden en de methode `action_create` aanroepen. + +### Patronen + +Het Kohana route systeem gebruikt perl compatibele reguliere expressies in zijn vergelijkings proces. Standaar worden de *keys* (omringd door `<>`) vergeleken met `[a-zA-Z0-9_]++` maar je kan je eigen patronen definiren voor elke key door een associatieve array mee te geven als extra argument aan [Route::set] met daarin de keys and patronen. We kunnen het vorige voorbeeld uitbreiden met een admin sectie en een filialen (affliates) sectie. Je kan deze in verschillende routes specificeren of je kan iets doen zoals dit: + + Route::set('sections', '(/(/(/)))', + array( + 'directory' => '(admin|affiliate)' + )) + ->defaults(array( + 'controller' => 'home', + 'action' => 'index', + )); + +Dit zorgt voor twee secties van uw site, 'admin' en 'affiliate', deze laten je toe om de controllers te organiseren in subfolders voor elk maar dat ze nog steeds blijven werken als de standaard route. + +### Meer Route voorbeelden + +Er zijn oneindig veel andere mogelijkheden voor routes. Hier zijn er enkele: + + /* + * Authenticatie + */ + Route::set('auth', '', + array( + 'action' => '(login|logout)' + )) + ->defaults(array( + 'controller' => 'auth' + )); + + /* + * Multi-formaat feeds + * 452346/comments.rss + * 5373.json + */ + Route::set('feeds', '(/).', + array( + 'user_id' => '\d+', + 'format' => '(rss|atom|json)', + )) + ->defaults(array( + 'controller' => 'feeds', + 'action' => 'status', + )); + + /* + * Statische pagina's + */ + Route::set('static', '.html', + array( + 'path' => '[a-zA-Z0-9_/]+', + )) + ->defaults(array( + 'controller' => 'static', + 'action' => 'index', + )); + + /* + * Je houdt niet van slashes? + * EditGallery:bahamas + * Watch:wakeboarding + */ + Route::set('gallery', '():', + array( + 'controller' => '[A-Z][a-z]++', + 'action' => '[A-Z][a-z]++', + )) + ->defaults(array( + 'controller' => 'Slideshow', + )); + + /* + * Vlug zoeken + */ + Route::set('search', ':', array('query' => '.*')) + ->defaults(array( + 'controller' => 'search', + 'action' => 'index', + )); + +Routes worden vergeleken in de gespecifieerde volgorde dus wees er van bewust dat als je routes insteld nadat de modules zijn ingeladen, een module een route kan specifiren dat voor een conflict zorgt met een route van jezelf. Dit is ook de reden waarom de standaard route als laatste wordt ingesteld, zodat zelfgeschreven routes eerst worden getest. + +### Request Parameters + +De directory, controller en action kunnen worden benaderd via de [Request] instantie op de volgende manieren: + + $this->request->action; + Request::instance()->action; + +Alle andere gespecifieerde keys in een route kunnen worden benaderd van binnenin de controller via: + + $this->request->param('key_name'); + +De [Request::param] methode heeft een optioneel tweede argument om een standaard waarde terug te geven indien de key niet is ingesteld door de route. Indien er geen argumenten worden gegeven, worden alle keys als teruggegeven als een associatieve array. + +### Conventie + +De gebruikelijke conventie is je eigen routes te plaatsen in het `MODPATH//init.php` bestand van je module als de routes bij een module horen, of gewoonweg te plaatsen in het `APPPATH/bootstrap.php` bestand boven de standaard route als de routes specifiek voor de applicatie zijn. Natuurlijk kunnen ze ook worden geimporteerd vanuit een extern bestand of zelfs dynamisch gegenereerd worden. + +## URLs + +Naast Kohana's sterke routing mogelijkheden zitten er ook enkele methodes in om URLs te genereren voor je routes' uris. Je kan je uris altijd specificeren als een string door gebruik te maken van [URL::site] om een volledige URL te maken: + + URL::site('admin/edit/user/'.$user_id); + +Kohana biedt echter ook een methode om de URI genereren op basis van de route's definitie. Dit is zeer handig als je routing ooit zou veranderen omdat het je zou verlossen van om overal uw code te veranderen waar je de URI als string hebt gespecificeerd. Hier is een voorbeeld van dynamische generatie die overeenkomt met het `feeds`-route voorbeeld van hierboven: + + Route::get('feeds')->uri(array( + 'user_id' => $user_id, + 'action' => 'comments', + 'format' => 'rss' + )); + +Laten we zeggen dat je later zou besluiten om die route definitie meer verstaanbaar te maken door ze te veranderen in `feeds/(/).`. Wanneer je je code hebt geschreven met de uri generatie methode van hierboven dan zal je niets moeten veranderen aan je code! Wanneer een deel van de URI tussen haakjes staat en waarvoor er geen waarde is meegegeven voor uri generatie en er geen standaard waarde is meegegeven in de route, dan zal dat stuk verwijderd worden van de uri. Een voorbeeld hiervan is het `(/)` deel van de standaard route, dit zal niet worden opgenomen in de gegenereerde uri als er geen id is voorzien. + +De methode [Request::uri] zal er n zijn dat je regelmatig zult gebruiken, het heeft dezelfde functionaliteit als hierboven maar het gaat gebruikt de huidige route, directory, controller en action. Als onze huidige route de standaard route is en de uri `users/list` is, dan kunnen we het volgende doen om uris te genereren in het formaat `users/view/$id`: + + $this->request->uri(array('action' => 'view', 'id' => $user_id)); + +Of een meer aangeraden methode voor in een view: + + Request::instance()->uri(array('action' => 'view', 'id' => $user_id)); + +## Links + +[!!] Nog geen informatie beschikbaar. diff --git a/includes/kohana/modules/userguide/guide/nl/using.autoloading.md b/includes/kohana/modules/userguide/guide/nl/using.autoloading.md new file mode 100644 index 00000000..e92d5831 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/using.autoloading.md @@ -0,0 +1,95 @@ +# Laden van Classes + +Kohana maakt dankbaar gebruik van PHP [autoloading](http://php.net/manual/language.oop5.autoload.php). Dit zorgt ervoor dat je niet [include](http://php.net/include) of [require](http://php.net/require) moet gebruiken vooraleer je de klasse kunt gebruiken. Bijvoorbeeld als je de [Cookie::set] method wilt gebruiken doe je: + + Cookie::set('mycookie', 'any string value'); + +Of om een [Encrypt] instantie in te laten, gewoon [Encrypt::instance] aanroepen: + + $encrypt = Encrypt::instance(); + +Classes worden ingeladen via de [Kohana::auto_load] methode, deze maakt een simpele conversie van de class naam naar de naam van het bestand: + +1. Classes worden geplaatst in de `classes/` folder van het [bestandssysteem](about.filesystem) +2. Ieder underscore karakter wordt omgezet naar een slash. +2. De bestandsnaam is met kleine letters + +Wanneer je een class aanroept die nog niet is ingeladen (vb. `Session_Cookie`), zal Kohana zoeken in het bestandssysteem via [Kohana::find_file] voor een bestand met de naam `classes/session/cookie.php`. + +## Zelfgeschreven Autoloaders + +De standaard autoloader wordt ingesteld in `application/bootstrap.php` via [spl_autoload_register](http://php.net/spl_autoload_register): + + spl_autoload_register(array('Kohana', 'auto_load')); + +Dit laat [Kohana::auto_load] toe om te proberen eender welke class in te laden dat nog niet bestaat wanneer de class voor het eerst wordt gebruikt. + +# Transparante Class Uitbreiding {#class-extension} + +Het [cascading bestandssyteem](about.filesystem) laat transparante class uitbreiding toe. Bijvoorbeeld, de class [Cookie] is gedefinieerd in `SYSPATH/classes/cookie.php` als: + + class Cookie extends Kohana_Cookie {} + +De standaard Kohana classes, en vele uitbreidingen, gebruiken deze manier van definiren zodat bijna alle classes kunnen worden uitgebreid. Je kan elke class transparant uitbreiden, door een eigen class te definiren in `APPPATH/classes/cookie.php` om je eigen methodes toe te voegen. + +[!!] Je past best **nooit** bestanden aan die standaard in Kohana zitten. Maak aanpassingen aan classes altijd door ze uit te breiden om upgrade-problemen te vermijden. + +Bijvoorbeeld, als je een methode wilt maken dat gecodeerde cookies maakt via de [Encrypt] class: + + encode((string) $value); + + parent::set($name, $value, $expiration); + } + + /** + * Krijg de inhoud van een gecodeerde cookie. + * + * @uses Cookie::get + * @uses Encrypt::decode + */ + public static function decrypt($name, $default = NULL) + { + if ($value = parent::get($name, NULL)) + { + $value = Encrypt::instance(Cookie::$encryption)->decode($value); + } + + return isset($value) ? $value : $default; + } + + } // End Cookie + +Als je nu `Cookie::encrypt('secret', $data)` aanroept zal die een een gecodeerde cookie aanmaken die je kan decoderen met `$data = Cookie::decrypt('secret')`. + +## Meerdere niveau's van uitbreidingen {#multiple-extensions} + +Als je een Kohana class in een module uitbreidt, maak je best gebruik van transparante uitbreidingen. In plaats van de [Cookie] uitbreiding Kohana te laten uitbreiden, kan je `MODPATH/mymod/encrypted/cookie.php` aanmaken: + + class Encrypted_Cookie extends Kohana_Cookie { + + // Gebruik de encrypt() en decrypt() methodes van hierboven + + } + +En maak `MODPATH/mymod/cookie.php` aan: + + class Cookie extends Encrypted_Cookie {} + +Dit laat nog steeds toe om gebruikers hun eigen uitbreidingen te laten doen op [Cookie] zodat jouw uitbreidingen nog behouden blijven. Let wel, de volgende uitbreiding van [Cookie] zal `Encrypted_Cookie` moeten uitbreiden in plaats van `Kohana_Cookie`. diff --git a/includes/kohana/modules/userguide/guide/nl/using.configuration.md b/includes/kohana/modules/userguide/guide/nl/using.configuration.md new file mode 100644 index 00000000..c9aa35db --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/using.configuration.md @@ -0,0 +1,57 @@ +# Algemene Configuratie + +Kohana gebruikt zowel static properties als bestanden worden gebruikt voor de configuratie. Static properties zijn worden meestal gebruikt voor static classes, zoals [Cookie], [Security] en [Upload]. Bestanden worden meestal gebruikt voor objecten zoals [Database], [Encrypt] en [Session]. + +Static properties kunnen ingesteld worden in `APPPATH/bootstrap.php` of door [class uitbreding](using.autoloading#class-extension). Het voordeel van static properties is dat er geen extra bestanden moeten worden ingeladen. Het probleem met deze methode is dat de class ingeladen word wanneer een property is ingesteld, als je geen uitbreiding gebruikt. Echter, met gebruik van uitbreidingen worden uitbreidingen uit modules overladen. Het is aanbevolen om static property te gebruiken voor configuraties in de bootstrap. + +[!!] Wanneer je opcode caching gebruikt, zoals [APC](http://php.net/apc) of [eAccelerator](http://eaccelerator.net/), dan is het inladen van classes merkbaar vermindert. Het is dan ook streng aanbevolen om opcode caching te bruiken bij *elke* website in productie, of die nu groot of klein is. + +## Noodzakelijke instellingen + +Bij iedere nieuwe Kohana installatie is het vereist om de [Kohana::init] instellingen aan te passen in `APPPATH/bootstrap.php`. Iedere instelling die niet specifiek is ingesteld zal de standaard instelling gebruiken. Deze instellingen kunnen aangeroepen worden en/of aangepast worden op een later tijdstip door de static property van de [Kohana] class te gebruiken. Bijvoorbeeld, om de huidige karakterset te verkrijgen lees je de [Kohana::$charset] property in. + +## Veiligheids instellingen + +Er zijn verschillende instellingen dat je moet veranden om Kohana veilig te maken. De belangrijkste is [Cookie::$salt], deze wordt gebruikt om een "handtekening" te maken op cookies zodat ze niet kunnen worden aangepast van buiten Kohana. + +Als je de [Encrypt] class wilt gebruiken, maak je best ook een `encrypt` configuratie bestand en stel je een encryption `key` in. Deze key bevat best letters, nummers en symbolen om de veiligheid te optimaliseren. + +[!!] **Gebruik geen hash als encryption key!** Indien je dit doet zal de encryption key gemakkelijker te kraken zijn. + +# Configuratie bestanden {#config-files} + +Configuratie bestanden zijn licht anders dan andere bestanden in het [cascading bestandssyteem](about.filesystem). Configuratie bestanden worden **gemerged** in plaats van overladen. Dit wil zeggen dat alle configuratie bestanden hetzelfde path worden gecombineerd om configuratie te vormen. Wat wil zeggen dat je *individuele* instellingen kan overladen in plaats van een volledig bestand te dupliceren. + +Configuratie bestanden zijn pure PHP bestanden, opgeslaan in de `config/` folder, die een associatieve array teruggeven: + + 'value', + 'options' => array( + 'foo' => 'bar', + ), + ); + +Als het bovenstaande bestand `myconf.php` werd genoemd, dan kon je deze benaderen via: + + $config = Kohana::config('myconf'); + $options = $config['options']; + +[Kohana::config] biedt ook een shortcut om individuele keys van configuratie arrays te benaderen door gebruik te maken van "dot paths". + +Verkrijg de "options" array: + + $options = Kohana::config('myconf.options'); + +Verkrijg de "foo" key van de "options" array: + + $foo = Kohana::config('myconf.options.foo'); + +Configuratie arrays kunnen ook worden benaderd als objecten, indien je deze manier wilt gebruiken: + + $options = Kohana::config('myconf')->options; + +Let wel, je kan enkel keys op het bovenste niveau aanspreken als object properties, alle lagere keys moeten benaderd worden via de standaard array syntax: + + $foo = Kohana::config('myconf')->options['foo']; diff --git a/includes/kohana/modules/userguide/guide/nl/using.messages.md b/includes/kohana/modules/userguide/guide/nl/using.messages.md new file mode 100644 index 00000000..2ba9a715 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/using.messages.md @@ -0,0 +1,26 @@ +# Berichten (Messages): de basis + +Kohana berichten zijn mensvriendelijke stukjes tekst voorgesteld door een korter woord of zin, een "key" genaamd. Berichten worden benaderd via de [Kohana::message] methode, die n enkel of een hele groep van berichten teruggeeft. + +Bijvoorbeeld, als een gebruiker niet is ingelogd en een pagina dat authenticatie vereist probeert te benaderen, dan moet een error zoals "U moet ingelogd zijn om toegang te hebben tot deze pagina" getoond worden. Dit bericht kan opgeslagen worden in het `auth` bestand met een `must_login` key: + + $message = Kohana::message('auth', 'must_login'); + +Berichten worden niet vertaald. Om een bericht te vertalen, gebruik dan de [translation function](using.translation): + + $translated = __(Kohana::message('auth', 'must_login')); + +[!!] In Kohana v2 werd het berichten-systeem gebruikt voor vertalingen. Echter is het ten zeerste aanbevolen om het nieuwe vertalingssysteem te gebruiken in plaats van berichten, aangezien het leesbare tekst teruggeeft wanneer zelfs geen vertaling beschikbaar is. + +## Berichten: de bestanden + +Alle berichten bestanden zijn pure PHP files, opgeslaan in de `messages/` folder, die een associatieve array teruggeven: + + 'U moet ingelogd zijn om toegang te hebben tot deze pagina', + 'no_access' => 'U heeft geen bevoegdheden om deze pagina te bekijken', + ); + +Berichten bestanden zijn gelijkaardig aan [configuratie bestanden](using.configuration#config-files) omdat ze ook worden samengevoegd. Dit betekent dat alle berichten die opgeslaan zijn in het bestand `auth` zullen worden gecombineerd in n enkele array, het is dus niet noodzakelijk om alle berichten te kopiren wanneer je een nieuw `auth` bestand aanmaakt. diff --git a/includes/kohana/modules/userguide/guide/nl/using.sessions.md b/includes/kohana/modules/userguide/guide/nl/using.sessions.md new file mode 100644 index 00000000..cdc4b178 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/using.sessions.md @@ -0,0 +1,223 @@ +# Gebruik van Sessies en Cookies + +Kohana biedt een paar classes die het gemakkelijk maken om te werken met cookies en sessies. Op een hoog niveau, zowel sessies en cookies geven dezelfde functionaliteit. Ze laten de ontwikkelaar toe om tijdelijke of blijvende informatie over een specifieke klant voor later op te slaan. + +Cookies moeten worden gebruikt voor de opslag van niet-private gegevens die persistent is voor een lange periode van tijd. Bijvoorbeeld het opslaan van een gebruikers-id of een taalvoorkeur. Gebruik de [Cookie] class voor het verkrijgen en instellen van cookies. + +[!!] Kohana gebruikt "ondertekende" cookies. Elke cookie die wordt opgeslagen wordt gecombineerd met een veilige hash om een wijziging van de cookie te voorkomen. Deze hash wordt gegenereerd met behulp van [Cookie:: salt], die de [Cookie::$salt] property gebruikt. Je moet [deze instelling] (using.configuration) veranderen wanneer je applicatie live staat. + +Sessies worden gebruikt voor het opslaan van tijdelijke of prive-gegevens. Zeer gevoelige gegevens moeten worden opgeslagen met behulp van de [Session] class met de "database" of "native" adapters. Bij gebruik van de "cookie"-adapter, moet de sessie altijd worden versleuteld. + +[!!] Voor meer informatie over de beste manieren van werken met sessie-variabelen, zie [the seven deadly sins of sessions](http://lists.nyphp.org/pipermail/talk/2006-December/020358.html). + +# Het opslaan, ophalen en verwijderen van gegevens + +[Cookie] en [Session] bieden een zeer gelijkaardige API voor het opslaan van gegevens. Het belangrijkste verschil tussen hen is dat sessies benaderd kunnen worden met behulp van een object, en cookies met behulp van een statische class. + +De sessie instantie benaderen wordt gedaan met de [Session::instance] methode: + + // Verkrijg de sessie instantie + $session = Session::instance(); + +Bij het gebruik van sessies, kan je alle huidige sessiegegevens krijgen met behulp van de [Session::as_array] methode: + + // Verkrijg alle sessiegegevens als een array + $data = $session->as_array(); + +Je kan dit ook gebruiken om de `$_SESSION` global te overladen om data te krijgen en in te stellen in verlijkbare manier zoals standaard PHP: + + // Overlaad $_SESSION met sessiegegevens + $_SESSION =& $session->as_array(); + + // Stel de sessiegegevens in + $_SESSION[$key] = $value; + +## Gegevens opslaan {#setting} + +Het opslaan van sessie- of cookie-gegevens wordt gedaan met behulp van de `set`-methode: + + // Sla sessiegegevens op + $session->set($key, $value); + + // Sla cookiegegevens op + Cookie::set($key, $value); + + // Sla een gebruikers id op + $session->set('user_id', 10); + Cookie::set('user_id', 10); + +## Verkrijgen van gegevens {#getting} + +Verkrijgen van sessie- of cookie-gegevens wordt gedaan met behulp van de `get`-methode: + + // Verkrijg sessiegegevens + $data = $session->get($key, $default_value); + + // Verkrijg cookiegegevens + $data = Cookie::get($key, $default_value); + + // Verkrijg het gebruikers id + $user = $session->get('user_id'); + $user = Cookie::get('user_id'); + +## Verwijderen van gegevens {#deleting} + +Het verwijderen van sessie- of cookie-gegevens wordt gedaan met behulp van de `delete`-methode: + + // Verwijderen van sessiegegevens + $session->delete($key); + + // Verwijderen van cookiegegevens + Cookie::delete($key); + + // Verwijder een gebruikers id + $session->delete('user_id'); + Cookie::delete('user_id'); + +# Configuratie {#configuration} + +Zowel cookies als sessies hebben verschillende configuratie-instellingen die van invloed zijn hoe gegevens worden opgeslagen. Controleer altijd deze instellingen voordat u uw applicatie live zet, omdat veel van die instellingen een rechtstreeks effect zal hebben op de veiligheid van uw applicatie. + +## Cookie Instellingen {#cookie-settings} + +Al de cookie instellingen worden verandert met behulp van statische properties. Je kan deze instellingen veranderen in `bootstrap.php` of door een [class extension](using.autoloading#class-extension) te gebruiken. + +De meest belangrijke instelling is [Cookie::$salt], die wordt gebruikt om veilig te ondertekenen. Deze waarde zou moeten gewijzigd en geheim gehouden worden: + + Cookie::$salt = 'Uw geheim is veilig bij mij'; + +[!!] Door het veranderen van deze waarde zullen alle bestaande cookies niet meer geldig zijn. + +Standaard worden cookies bewaard tot het browservenster wordt gesloten. Om een specifieke leeftijd te gebruiken, verander de [Cookie::$expiration] instelling: + + // Stel in dat cookies vervallen na n week + Cookie::$expiration = 604800; + + // Alternatief voor het gebruik van getallen, voor meer duidelijkheid + Cookie::$expiration = Date::WEEK; + +Het path waarvan de cookie kan worden opgevraagd kan worden beperkt met behulp van de [Cookie::$path] instelling. + + // Enkel cookies toelaten wanneer je gaat naar /public/* + Cookie::$path = '/public/'; + +Het domein waarvan de cookie kan worden geopend kan ook worden beperkt, met behulp van de [Cookie::$domain] instelling. + + // Enkel cookies toelaten voor www.example.com + Cookie::$domain = 'www.example.com'; + +Als u de cookie toegankelijk wilt maken op alle subdomeinen, gebruik dan een punt aan het begin van het domein. + + // Cookies toegankelijk maken voor example.com en *.example.com + Cookie::$domain = '.example.com'; + +Als je de cookie alleen wilt kunnen benaderen via een beveiligde (HTTPS) verbinding, gebruik dan de [Cookie::$secure] instelling. + + // Cookies enkel toegangekijk maken via een beveiligde verbinding + Cookie::$secure = TRUE; + + // Cookies toegankelijk maken voor elke verbinding + Cookie::$secure = FALSE; + +Om te voorkomen dat cookies worden geopend met behulp van Javascript, kunt u de [Cookie::$httponly] instelling aanpassen. + + // Maak cookies niet toegankelijk via Javascript + Cookie::$httponly = TRUE; + +## Sessie Adapters {#adapters} + +Bij het maken van of het aanroepen van een instantie van de [Sessie] class kan je kiezen welke sessie adapter je wilt gebruiken. De sessie adapters die beschikbaar zijn voor je: + +Native +: Slaat sessiegegevens op in de standaard locatie voor uw web server. De opslaglocatie is gedefinieerd door [session.save_path](http://php.net/manual/session.configuration.php#ini.session.save-path) in `php.ini` of gedefinieerd door [ini_set](http://php.net/ini_set). + +Database +: Slaat de sessiesgegevens op in een database tabel door gebruik te maken van de [Session_Database] class. De [Database] module is vereist. + +Cookie +: Slaat de sessiegegevens op in een cookie door gebruikt te maken van de [Cookie] class. **Sessies hebben een 4KB limiet wanneer je deze adapter gebruikt.** + +De standaard adapter kan ingesteld worden door de waarde aan te passen van [Session::$default]. De standaard adapter is "native". + +[!!] Zoals bij cookies bekent een "lifetime" instelling van "0" dat de sessie zal vervallen bij het sluiten van de het browservenster. + +### Sessie Adapter Instellingen + +Je kan configuratie-instellingen voor elk van de sessie adapters instellen door het creren van een sessie configuratiebestand in `APPPATH/config/session.php`. Het volgende voorbeeld van een configuratie bestand definiert alle instellingen voor elke adapter: + + return array( + 'native' => array( + 'name' => 'session_name', + 'lifetime' => 43200, + ), + 'cookie' => array( + 'name' => 'cookie_name', + 'encrypted' => TRUE, + 'lifetime' => 43200, + ), + 'database' => array( + 'name' => 'cookie_name', + 'encrypted' => TRUE, + 'lifetime' => 43200, + 'group' => 'default', + 'table' => 'table_name', + 'columns' => array( + 'session_id' => 'session_id', + 'last_active' => 'last_active', + 'contents' => 'contents' + ), + 'gc' => 500, + ), + ); + +#### Native Adapter {#adapter-native} + +Type | Instelling | Omschrijving | Standaard +----------|------------|-----------------------------------------------------|----------- +`string` | name | naam van de sessie | `"session"` +`integer` | lifetime | aantal seconden dat de sessie moet bestaan | `0` + +#### Cookie Adapter {#adapter-cookie} + +Type | Instelling | Omschrijving | Standaard +----------|------------|-----------------------------------------------------|----------- +`string` | name | naam van de cookie om de sessiegegevens op te slaan | `"session"` +`boolean` | encrypted | de sessiegegevens coderen met [Encrypt]? | `FALSE` +`integer` | lifetime | aantal seconden dat de sessie moet bestaan | `0` + +#### Database Adapter {#adapter-database} + +Type | Instelling | Omschrijving | Standaard +----------|------------|-----------------------------------------------------|----------- +`string` | group | [Database::instance] groep naam | `"default"` +`string` | table | de tabelnaam waar de gegevens worden in opgeslagen | `"sessions"` +`array` | columns | associatieve array met kolom aliassen | `array` +`integer` | gc | 1:x kans dat de garbage collection uitgevoerd wordt | `500` +`string` | name | naam van de cookie om de sessiegegevens op te slaan | `"session"` +`boolean` | encrypted | de sessiegegevens coderen met [Encrypt]? | `FALSE` +`integer` | lifetime | aantal seconden dat de sessie moet bestaan | `0` + +##### Tabel Schema + +Je moet de sessie-opslag tabel in de database aanmaken. Dit is het standaard schema: + + CREATE TABLE `sessions` ( + `session_id` VARCHAR(24) NOT NULL, + `last_active` INT UNSIGNED NOT NULL, + `contents` TEXT NOT NULL, + PRIMARY KEY (`session_id`), + INDEX (`last_active`) + ) ENGINE = MYISAM; + +##### Tabel kolommen + +Je kunt de namen van kolommen aanpassen om overeen te komen met een bestaand database-schema. De standaard waarde is hetzelfde als de key waarde. + +session_id +: de naam van de "id" kolom + +last_active +: UNIX timestamp van het laatste tijdstip dat de sessie werd aangepast + +contents +: sessiongegevens opgeslaan in een serialized string, en optioneel gecodeerd diff --git a/includes/kohana/modules/userguide/guide/nl/using.views.md b/includes/kohana/modules/userguide/guide/nl/using.views.md new file mode 100644 index 00000000..beef852b --- /dev/null +++ b/includes/kohana/modules/userguide/guide/nl/using.views.md @@ -0,0 +1,118 @@ +# Het gebruik van Views + +Views zijn bestanden die de visuele informatie bevatten voor je applicatie. Dit is meestal HTML, CSS en Javascript maar kan van alles zijn die je nodig hebt zoals XML of JSON voor AJAX output. Het doel van views is om deze informatie af te scheiden van de applicatie logica zodat je nettere code hebt en deze gemakkelijker kunt hergebruiken. + +Hoewel dit waar is, kunnen views zelf ook code bevatten die je gebruikt om gegevens te tonen die je meegestuurd hebt met de view. Bijvoorbeeld, het loopen door een array met producten en voor elk product een nieuwe tabelrij tonen. Views zijn nog altijd PHP bestanden dus kan je erin coderen zoals je normaal zou doen. + +# Aanmaken van View bestanden + +De View bestanden worden opgeslagen in de `views` folder van het [bestandssysteem](about.filesystem). Je kan ook subfolders aanmaken in de `views` folder om je bestanden meer te organiseren. Alle mogelijkheden uit de volgende voorbeelden zijn goed: + + APPPATH/views/home.php + APPPATH/views/pages/about.php + APPPATH/views/products/details.php + MODPATH/error/views/errors/404.php + MODPATH/common/views/template.php + +## Inladen van Views + +[View] objecten worden gewoonlijk aangemaakt binnenin een [Controller] via de [View::factory] methode. De view wordt dan gewoonlijk aan de [Request::$response] property toegewezen of aan een andere view. + + public function action_about() + { + $this->request->response = View::factory('pages/about'); + } + +Wanneer een view wordt toegewezen aan de [Request::$response], zoals in bovenstaand voorbeeld, dan zal het automatisch worden gerenderd wanneer noodzakelijk. Om het gerenderde resultaat van een view te verkrijgen kan je de [View::render] methode aanspreken of gewoon laten casten naar een string. Wanneer een view gerenderd is, wordt de view ingeladen en wordt de HTML gegenereerd. + + public function action_index() + { + $view = View::factory('pages/about'); + + // View wordt gerenderd + $about_page = $view->render(); + + // Of gewoon laten casten naar een string + $about_page = (string) $view; + + $this->request->response = $about_page; + } + +## Variabelen in Views + +Eenmaal een view is ingeladen, kunnen variabelen eraan toegewezen worden door de [View::set] en [View::bind] methodes. + + public function action_roadtrip() + { + $view = View::factory('user/roadtrip') + ->set('places', array('Rome', 'Paris', 'London', 'New York', 'Tokyo')); + ->bind('user', $this->user); + + // De view zal de variabelen $places en $user hebben + $this->request->response = $view; + } + +[!!] Het enige verschil tussen `set()` en `bind()` is dat `bind()` de variabele toewijst via referentie. Als je een variabele `bind()` vooraleer ze gedefineerd is, zal de variable als `NULL` worden gecreerd. + +### Globale Variabelen + +Een applicatie kan verschillende views hebben die toegang hebben tot dezelfde variabelen. Bijvoorbeeld, een titel van een pagina wil je zowel tonen in de header van je template als in de body van de pagina inhoud. Je kan variabelen creren dat toegankelijk zijn in elke view dankzij de [View::set_global] en [View::bind_global] methoden. + + // Wijs $page_title toe aan alle views + View::bind_global('page_title', $page_title); + +Als de applicatie drie views heeft die gerenderd zijn voor de home-pagina: `template`, `template/sidebar` en `pages/home`. Eerst zal je een abstracte controller maken om de template te maken: + + abstract class Controller_Website extends Controller_Template { + + public $page_title; + + public function before() + { + parent::before(); + + // Maak $page_title toegankelijk in alle views + View::bind_global('page_title', $this->page_title); + + // Laad $sidebar in de template als een view + $this->template->sidebar = View::factory('template/sidebar'); + } + + } + +Dan moet de home controller de `Controller_Website` uitbreiden: + + class Controller_Home extends Controller_Website { + + public function action_index() + { + $this->page_title = 'Home'; + + $this->template->content = View::factory('pages/home'); + } + + } + +## Views in Views + +Als je een andere view wilt gebruiken in een view heb je twee keuzes. Door [View::factory] aan te roepen kan je de opgenomen view sandboxen. Dit betekent dat je alle variabelen moet meegeven aan de view door middel van [View::set] of [View::bind]: + + // Enkel de $user variabele zal toegankelijk zijn in "views/user/login.php" + bind('user', $user) ?> + +De andere optie is om de view rechtstreeks in te voegen, dat maakt alle huidige variabelen beschikbaar in de ingesloten view: + + // Elke variabele gedefinieerd in deze view zal worden ingesloten in "views/message.php" + + +Natuurlijk kan je ook een volledige [Request] inladen in een view: + + execute() ?> + +Dit is een voorbeeld van [HMVC](about.mvc), dit maakt het mogelijk om aanroepingen te maken en te lezen via andere URLs binnenin je applicatie. + +# Upgraden van v2.x + +In tegenstelling tot versie 2.x van Kohana, wordt de view niet ingeladen in de context van de [Controller], dus is het niet mogelijk om `$this` aan te spreken als controller binnenin de view. De controller doorgeven aan de view moet nu expliciet worden gedaan: + + $view->bind('controller', $this); diff --git a/includes/kohana/modules/userguide/guide/ru-ru/about.conventions.md b/includes/kohana/modules/userguide/guide/ru-ru/about.conventions.md new file mode 100644 index 00000000..24a6904e --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/about.conventions.md @@ -0,0 +1,301 @@ +# Соглашения + +При работе с фреймворком приветствуется использование Kohana [coding style](http://dev.kohanaphp.com/wiki/kohana2/CodingStyle). В нем используется [BSD/Allman стиль](http://ru.wikipedia.org/wiki/%D0%9E%D1%82%D1%81%D1%82%D1%83%D0%BF_%28%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%29#.D0.A1.D1.82.D0.B8.D0.BB.D1.8C_.D0.9E.D0.BB.D0.BC.D0.B0.D0.BD.D0.B0) расстановки фигурных скобок и прочего форматирования кода. + +## Имена классов и расположение файлов {#classes} + +Для облегчения работы [автозагрузки](about.autoloading) в Kohana, имена классов строго регламентированы. Имя класса должно начинаться с заглавной буквы, а для разделения слов в имени используется нижнее_подчёркивание. Нижнее подчёркивание отражает расположение файла в файловой структуре проекта. + +Данные соглашения предусматривают: + +1. Не рекомендуется использовать CamelCase в имени класса, за исключением случаев, когда нежелательно создавать дополнительные поддиректории. +2. Все файлы классов и директории должны быть в нижнем регистре. +3. Все классы должны располагаться в директории `classes`, которая может быть на любом уровне [каскадной файловой системы](about.filesystem) фреймворка. + +[!!] В отличие от Kohana версии 2.x, в Kohana 3 нет разделения между "типами" классов: будь то "контроллер", "модель", "библиотека" или "хелпер". Все классы располагаются в директории "classes/" вне зависимости от того, являются ли они статическими "хелперами" или объектами "библиотеки". Вы вправе реализовывать любой дизайн класса, который Вам необходим: статический, синглтон, адаптер и т.п. + +## Примеры + +Помните, нижнее подчёркивание в наименовании класса отвечает за разделение на директории и отражает расположение файла в файловой структуре проекта. + +Название класса | Файл +----------------------|------------------------------- +Controller_Template | classes/controller/template.php +Model_User | classes/model/user.php +Database | classes/database.php +Database_Query | classes/database/query.php +Form | classes/form.php + +## Стандарты кодирования {#coding_standards} + +Для того, чтобы Ваш код был удобочитаемым и выглядел последовательным, мы просим всех на максимально придерживаться стандартов кодирования. + +### Фигурные скобки + +Используйте, пожалуйста, [BSD/Allman стиль](http://ru.wikipedia.org/wiki/%D0%9E%D1%82%D1%81%D1%82%D1%83%D0%BF_%28%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5%29#.D0.A1.D1.82.D0.B8.D0.BB.D1.8C_.D0.9E.D0.BB.D0.BC.D0.B0.D0.BD.D0.B0) расстановки фигурных скобок. + +### Соглашение в именовании классов + +В Kohana используется нижнее_подчёркивание в именовании классов. Именование с использованием camelCase не приветствуется. + +#### Классы + + // Корневые библиотеки используют суффикс _Core + class Beer_Core { + + // При расширение функционала библиотеки суффикс не используется + class Beer extends Beer_Core + + // Класс контроллера использует суффикс _Controller + class Apple_Controller extends Controller { + + // Класс модели использует суффикс _Model + class Cheese_Model extends Model { + + // Класс хелпера + class peanut { + +Если Вы не передаёте параметры в конструктор при создании класса, не используйте круглые скобки при его объявлении: + + // Правильно: + $db = new Database; + + // Не правильно: + $db = new Database(); + +#### Функции и методы + +Имена функций должны быть в нижнем регистре и использовать нижнее_подчёркивание для разделения слов: + + function drink_beverage($beverage) + { + +#### Переменные + +Все переменные должны быть в нижнем регистре и использовать нижнее_подчёркивание для разделения слов (camelCase использовать запрещено): + + // Правильно: + $foo = 'bar'; + $long_example = 'uses underscores'; + + // Не правильно: + $weDontWantThis = 'understood?'; + +### Отступы + +Используйте табуляцию для отступов в Вашем коде. Использование пробелов вместо табуляции строго запрещено. + +Вертикальные отступы (при многострочном выравнивании) формируются пробелами. Так как ширина табуляции может отличаться у разных людей, для вертикального выравнивания табуляция является не самым лучшем способом. + + $text = 'это разделённый на несколько строк длинный текст. Зачастую, ' + . 'при многострочном разделении используют ограничение в 80 знаков ' + . 'на строку. Для удобочитаемости кода крайне важно применять правильное ' + . 'вертикальное выравнивание текста. Помните, что любые отступы ' + . 'должны выполняться в виде табуляции, а вертикальное выравнивание уже ' + . 'должно завершаться пробелами, которые идут сразу после табов .'; + +### Конкатенация строк + +Не используйте пробелы вокруг оператора конкатенации: + + // Правильно: + $str = 'one'.$var.'two'; + + // Не правильно: + $str = 'one'. $var .'two'; + $str = 'one' . $var . 'two'; + +### Однострочные операторы + +Однострочные операторы IF могут быть использованы лишь в случае прерывания выполнения кода (например, return или continue): + + // Допустимо: + if ($foo == $bar) + return $foo; + + if ($foo == $bar) + continue; + + if ($foo == $bar) + break; + + if ($foo == $bar) + throw new Exception('You screwed up!'); + + // Не допустимо: + if ($baz == $bun) + $baz = $bar + 2; + +### Операторы сравнения + +Используйте, пожалуйста, для сравнения операторы OR и AND: + + // Правильно: + if (($foo AND $bar) OR ($b AND $c)) + + // Не правильно: + if (($foo && $bar) || ($b && $c)) + +Используйте, пожалуйста, elseif вместо else if: + + // Правильно: + elseif ($bar) + + // Не правильно: + else if($bar) + +### Конструкции switch + +Каждый оператор case, break и default должны располагаться на новой строке. Блок внутри case и default должны иметь отступ в 1 таб. + + switch ($var) + { + case 'bar': + case 'foo': + echo 'hello'; + break; + case 1: + echo 'one'; + break; + default: + echo 'bye'; + break; + } + +### Круглые скобки + +Необходимо отделять имя оператора и следующие за ним скобки пробелом. Оператор ! (оператор отрицания, восклицательный знак) для удобочитаемости должен быть выделен пробелом с обеих сторон. + + // Правильно: + if ($foo == $bar) + if ( ! $foo) + + // Не правильно: + if($foo == $bar) + if(!$foo) + if ((int) $foo) + if ( $foo == $bar ) + if (! $foo) + +### Тернарные операторы + +Все тернарные операции должны придерживаться стандартного формата. Используйте круглые скобки только для выделения условий, а не для выделения простых переменных. + + $foo = ($bar == $foo) ? $foo : $bar; + $foo = $bar ? $foo : $bar; + +Все сравнения и операции должны быть заключены в круглые скобки: + + $foo = ($bar > 5) ? ($bar + $foo) : strlen($bar); + +При многострочном разделении тернарного комплекса (при очень длинных условиях, превышающих длину в 80 знаков), для вертикального выравнивания стоит использовать пробелы. Операторы при этом должны располагаться один под другим: + + $foo = ($bar == $foo) + ? $foo + : $bar; + +### Приведение типов + +При приведении типов, следует использовать пробелы вокруг оператора приведения: + + // Правильно: + $foo = (string) $bar; + if ( (string) $bar) + + // Не правильно: + $foo = (string)$bar; + +По возможности, используйте приведение типов вместо тернарных операторов: + + // Правильно: + $foo = (bool) $bar; + + // Не правильно: + $foo = ($bar == TRUE) ? TRUE : FALSE; + +При приведение к числовому или булеву типам, используйте сокращённый формат: + + // Правильно: + $foo = (int) $bar; + $foo = (bool) $bar; + + // Не правильно: + $foo = (integer) $bar; + $foo = (boolean) $bar; + +### Константы + +Имена констант должны быть строго в верхнем регистре: + + // Правильно: + define('MY_CONSTANT', 'my_value'); + $a = TRUE; + $b = NULL; + + // Не правильно: + define('MyConstant', 'my_value'); + $a = True; + $b = null; + +При сравнении располагайте константы в конце: + + // Правильно: + if ($foo !== FALSE) + + // Не правильно: + if (FALSE !== $foo) + +Такой выбор немного спорный, но можно его объяснить следующим. Если вербализировать предыдущие условия и написать их, то можно их прочитать следующим образом для верного примера: + + если переменная $foo в точности не является FALSE + +И для неверного: + + если FALSE в точности не является переменной $foo + +Так как наш язык предусматривает чтение слева направо, то и нет причин для того, чтобы ставить константу первой при сравнении. + +### Комментарии + +#### Однострочные комментарии + +Используйте // перед строкой комментария, делайте отступ и начинайте комментарий с заглавной буквы. Никогда не используйте для комментариев #. + + // Правильно + + //Не правильно + // не правильно + # Не правильно + +### Регулярные выражения + +При использовании регулярных выражений, используйте синтаксис PCRE вместо POSIX. PCRE признан более мощной и быстрой библиотекой регулярных выражений. + + // Правильно: + if (preg_match('/abc/i'), $str) + + // Не правильно: + if (eregi('abc', $str)) + +Используйте одинарные кавычки вокруг регулярного выражения. Одинарные кавычки удобочитаемы ввиду их простоты. Строки, заключённые в одинарные кавычки, в отличие от двойных кавычек, не поддерживают интерполяцию переменных и конструкции с обратным слешем (\n, \t и т.д.). + + // Правильно: + preg_match('/abc/', $str); + + // Не правильно: + preg_match("/abc/", $str); + +При использовании регулярного выражения для поиска и замены, используйте $n нотацию для ссылок. Это более предпочтительно, нежели \\n. + + // Правильно: + preg_replace('/(\d+) dollar/', '$1 euro', $str); + + // Не правильно: + preg_replace('/(\d+) dollar/', '\\1 euro', $str); + +И в заключении, обратите внимание, что символ $ для определения позиции в конце строки не учитывает наличие символа новой строки. Используйте при необходимости модификатор D, чтобы этого избежать. [Подробная информация](http://blog.php-security.org/archives/76-Holes-in-most-preg_match-filters.html). + + $str = "email@example.com\n"; + + preg_match('/^.+@.+$/', $str); // TRUE + preg_match('/^.+@.+$/D', $str); // FALSE \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/ru-ru/about.filesystem.md b/includes/kohana/modules/userguide/guide/ru-ru/about.filesystem.md new file mode 100644 index 00000000..03f90b07 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/about.filesystem.md @@ -0,0 +1,59 @@ +# Каскадная файловая система + +Файловая система Kohana представляет собой иерархическая структура каталогов. Когда файл загружается, используя [Kohana::find_file], он ищется в каталогах в следующем порядке: + +Application путь +: Определён как `APPPATH` в `index.php`. Значение по-умолчанию - `application`. + +Module путь +: Определён как ассоциативный массив [Kohana::modules] в `APPPATH/bootstrap.php`. Поиск каждого значение массива будет произведён в том порядке, в каком порядке они определены. + +System путь +: Определён как `SYSPATH` в `index.php`. Значение по-умолчанию - `system`. Все основные или "core" файлы и классы располагаются в этой категории. + +Файлы, находящиеся в каталогах выше уровнем, имеют приоритет над файлами, которые располагаются в таких же папках уровнем ниже, что позволяет перегружать любой файл, путём перемещения файла с таким же названием на уровень выше: + +![Каскадная файловая система](img/cascading_filesystem.png) + +Если у Вас имеется файл представления с названием `wellcome.php`, расположенный в `APPPATH/views` и `SYSPATH/views`, то при поиске файла с этим именем будет возвращен тот, который находится в папке `APPPATH/views` (как находящийся на высшем уровне иерархической системы). Если же удалить файл в каталоге `APPPATH/views`, то при очередном поиске этого файла будет вызван тот, что располагается в каталоге `SYSPATH/views`. + +## Типы файлов + +Верхний уровень каталогов application, module, и system путей, по-умолчанию имеет следующие директории: + +classes/ +: Все классы, которые должны быть [автоматически загружены](using.autoloading) должны располагаться тут. Это касается контроллеров, моделей и всех других классов. Наименования классов должны соответствовать [соглашению по именованию классов](about.conventions#classes). + +config/ +: Конфигурационные файлы возвращают ассоциативный массив опций, который может быть загружен с использованием метода [Kohana::config]. Для более подробной информации, обратитесь к разделу [Настройка](using.configuration). + +i18n/ +: Файлы перевода возвращают ассоциативный массив строк. Перевод осуществляется с использованием метода `__()`.Для того, чтобы перевести "Hello, world!" на Русский, нужно вызвать `__('Hello, world!')` c [I18n::$lang] определённом как "ru-ru". Для более подробной информации, обратитесь к разделу [Интернационализация](using.translation). + +messages/ +: Файлы сообщений возвращают ассоциативный массив строк, который может быть загружен с использованием [Kohana::message]. Разница между сообщениями и файлами интернационализации заключается в том, что сообщения никогда не переводятся, пишутся на языке, используемом по-умолчанию, и вызываются по односложному ключу. Для более подробной информации, обратитесь к разделу [Сообщения (Messages)](using.messages). + +views/ +: Файлы представления являются PHP файлами, которые используются для генерации HTML или другого вида вывода информации. Файлы представления загружаются в объект [View] и присваиваются переменным, которые в дальнейшем конвертируются во фрагменты HTML. Множественные файлы представления могут быть вызваны одни в других. Для более подробной информации, обратитесь к разделу [Представление и HTML](usings.views). + +## Поиск файлов + +Путь к любому файлу в файловой структуре приложения может быть найден путём вызова [Kohana::find_file]: + + // Ищет полный путь до "classes/cookie.php" + $path = Kohana::find_file('classes', 'cookie'); + + // Ищет полный путь до "views/user/login.php" + $path = Kohana::find_file('views', 'user/login'); + +# Сторонние библиотеки + +Мы называем библиотеки, которые не специфичны для Kohana "vendor" библиотеками. Например, если Вы хотите использовать [DOMPDF](http://code.google.com/p/dompdf), то Вам нужно скопировать эту библиотеку в `application/vendor/dompdf` и добавить автозагрузочный класс DOMPDF: + + require Kohana::find_file('vendor', 'dompdf/dompdf/dompdf_config.inc'); + +После этого, можно использовать DOMPDF без загрузки каких-либо других файлов: + + $pdf = new DOMPDF; + +[!!] Если Вы хотите конвертировать представление в PDF с использованием DOMPDF, используйте [PDFView](http://github.com/shadowhand/pdfview) модуль. \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/ru-ru/about.flow.md b/includes/kohana/modules/userguide/guide/ru-ru/about.flow.md new file mode 100644 index 00000000..28c1186d --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/about.flow.md @@ -0,0 +1,73 @@ +# Порядок выполнения + +Каждое приложение выполняется в следующем порядке: + +1. Старт приложения из `index.php` +2. Установка путей для application, module, и system. +3. Установка уровня Error reporting. +4. Загружается файл установки, если таковой имеется. +5. Загружается класс [Kohana]. +6. Подключение `APPPATH/bootstrap.php` +7. Вызывается [Kohana::init], который устанавливает обработку ошибок, кэширование и логирование. +8. Подгружаются [Kohana_Config] и [Kohana_Log]. +9. Вызывается [Kohana::modules] для активации дополнительных модулей. + * Информация о пути к модулям добавляется в [каскадную файловую систему](about.filesystem). + * Если находится, подгружается файл `init.php` модуля. + * Файл `init.php` производит дополнительную настройку окружения, включая добавление маршрутов. +10. Несколько раз вызывается [Route::set], чтобы определить все [маршруты](using.routing) приложения. +11. Вызывается [Request::instance], чтобы начать обработку выполнения. + 1. Проверяет все маршруты, пока не найдёт совпадения с вызываемым. + 2. Создаёт инстанс контроллера и передаёт ему запрос. + 3. Вызывает метод [Controller::before]. + 4. Вызывает действие контроллера, которое генерирует ответа на запрос. + 5. Вызывает метод [Controller::after]. + * Предыдущие 5 шагов могут быть вызваны несколько раз при использовании [HMVC подзапросов](about.mvc). +12. Отображается результат ответа на запрос ([Request]). + +## index.php + +Kohana использует в работе паттерн [front controller], что означает, что все запросы направляются на `index.php`. Это позволяет использовать понятный и прозрачный дизайн [файловой системы](about.filesystem). `index.php` содержит лишь основные возможные конфигурационные свойства. Вы лишь можете изменить `$application`, `$modules`, и `$system` пути и выставить уровень оповещения об ошибках. + +Переменная `$application` позволяет определить директорию, которая содержит файлы Вашего приложения. По-умолчанию - это `application`. Переменная `$modules`, соответственно, указывает на директорию с файлами модулей, а `$system` - директорию с файлами ядра Kohana. + +Вы можете переместить эти директории куда угодно. Например, если Вы имеете такую структуру каталогов: + + www/ + index.php + application/ + modules/ + system/ + +То можете переместить директории на уровень выше корня web папки: + + application/ + modules/ + system/ + www/ + index.php + +После этого нужно привести установки в `index.php` к следующему виду: + + $application = '../application'; + $modules = '../modules'; + $system = '../system'; + +Теперь ни один из каталогов не может быть напрямую доступен для web сервера. Совсем не обязательно производить эти изменения, но, помимо прочего, это может быть полезно при использовании одного набора директорий среди множества приложений. + +[!!] В начале всех файлов Kohana присутствует проверка безопасности, дабы предотвратить доступ до файла без использования front контроллера. Однако, более безопасно разместить application, modules, и system директории таким образом, чтобы они были недоступны извне. + +### Сообщения об ошибках + +По-умолчанию, Kohana отображает все ошибки, включая strict mode предупреждения. Это устанавливается, используя [error_reporting](http://php.net/error_reporting): + + error_reporting(E_ALL | E_STRICT); + +Когда Ваше приложение опубликовывается в продуктив, то рекомендуется использовать более консервативные настройки, такие как игнорирование уведомлений: + + error_reporting(E_ALL & ~E_NOTICE); + +Если Вы получаете белый экран при ошибке, то это означает, что Ваш сервер имеет отключённое свойство отображение ошибок. Вы можете включить его, добавив эти строчки сразу после вызова `error_reporting`: + + ini_set('display_errors', TRUE); + +Ошибки должны **всегда** отображаться, даже в продуктиве, так как это позволяет использовать [исключения и обработку ошибок](debugging.errors), чтобы выдавать симпатичную страничку с ошибкой, нежели дезориентирующий белый экран. \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/ru-ru/about.install.md b/includes/kohana/modules/userguide/guide/ru-ru/about.install.md new file mode 100644 index 00000000..7380a99a --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/about.install.md @@ -0,0 +1,89 @@ +# Установка + +1. Загрузите последний **стабильный** релиз с [сайта Kohana](http://kohanaframework.org/) +2. Распакуйте загруженный архив (появится директория `kohana`) +3. Загрузите содержание архива на Ваш web-сервер +4. Откройте файл `application/bootstrap.php` и произведите следующие изменения: + - Установите [часовой пояс](http://php.net/timezones) (timezone), который будет использоваться по-умолчанию в Вашем приложении + - Установите `base_url` в параметрах вызова [Kohana::init], чтобы обозначить расположение фреймворка на Вашем сервере +6. Убедитесь в том, что папки `application/cache` и `application/logs` доступны для записи (для *nix ОС воспользуйтесь командой `chmod application/{cache,logs} 0777`) +7. Проверьте правильность установки, открыв URL, указанный Вами ранее как `base_url` в Вашем любимом браузере + +[!!] В зависимости от платформы, установленные поддиректории могут потерять значения прав доступа из-за особенностей процесса zip распаковки. Чтобы выправить права доступа, измените права на 755, выполнив в командной строке `find . -type d -exec chmod 0755 {} \;` из корневой директории Kohana. + +Вы увидите страницу установки. Если будут отображены какие-либо ошибки, необходимо их устранить перед тем, как продолжать работать. + +![Страница установки](img/install.png "Пример страницы установки") + +После того, как Вы убедитесь, что все сконфигурировано правильно, переименуйте или удалите файл `install.php`. После этого Вы увидите приветственную страницу Kohana: + +![Страница приветствия](img/welcome.png "Example of welcome page") + +## Настройка продуктив-окружения + +Имеется несколько вещеё, которые Вы наверняка захотите сделать перед публикацией Вашего приложения. + +1. Прочитайте описание процесса [настройки](about.configuration) этой документации. Оно охватывает большинство глобальных настроек, которые требуют изменения при смене окружения. Основное правило для сайтов в продуктиве - это активация кэширования и отключение профилирования (свойства [Kohana::init]). [Кэширование маршрутов](api/Route#cache) так же может быть полезным при наличии большого числа маршрутов. +2. Обрабатывайте все исключения в `application/bootstrap.php` таким образом, чтобы не было утечки конфиденциальной информации при попытках трассировки запросов. Изучите нижеизложенный пример, который был взят из [исходных кодов сайта wingsc.com](http://github.com/shadowhand/wingsc), написанного Shadowhand'ом. +3. Включите APC или любой другой вид кэширования кода. Это единственный и самый простой способ увеличения производительности, который можно применить к самому PHP. Чем сложнее и больше Ваше приложение, тем больше выгода от использования кэширования кода. + + /** + * Set the environment string by the domain (defaults to Kohana::DEVELOPMENT). + */ + Kohana::$environment = ($_SERVER['SERVER_NAME'] !== 'localhost') ? Kohana::PRODUCTION : Kohana::DEVELOPMENT; + /** + * Initialise Kohana based on environment + */ + Kohana::init(array( + 'base_url' => '/', + 'index_file' => FALSE, + 'profile' => Kohana::$environment !== Kohana::PRODUCTION, + 'caching' => Kohana::$environment === Kohana::PRODUCTION, + )); + + /** + * Execute the main request using PATH_INFO. If no URI source is specified, + * the URI will be automatically detected. + */ + $request = Request::instance($_SERVER['PATH_INFO']); + + try + { + // Attempt to execute the response + $request->execute(); + } + catch (Exception $e) + { + if (Kohana::$environment === Kohana::DEVELOPMENT) + { + // Just re-throw the exception + throw $e; + } + + // Log the error + Kohana::$log->add(Kohana::ERROR, Kohana::exception_text($e)); + + // Create a 404 response + $request->status = 404; + $request->response = View::factory('template') + ->set('title', '404') + ->set('content', View::factory('errors/404')); + } + + if ($request->send_headers()->response) + { + // Get the total memory and execution time + $total = array( + '{memory_usage}' => number_format((memory_get_peak_usage() - KOHANA_START_MEMORY) / 1024, 2).'KB', + '{execution_time}' => number_format(microtime(TRUE) - KOHANA_START_TIME, 5).' seconds'); + + // Insert the totals into the response + $request->response = str_replace(array_keys($total), $total, $request->response); + } + + + /** + * Display the request response. + */ + echo $request->response; + diff --git a/includes/kohana/modules/userguide/guide/ru-ru/about.kohana.md b/includes/kohana/modules/userguide/guide/ru-ru/about.kohana.md new file mode 100644 index 00000000..6367779e --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/about.kohana.md @@ -0,0 +1,15 @@ +# Что такое Kohana? + +Kohana это открытый, [объектно-ориентированный](http://ru.wikipedia.org/wiki/Объектно-ориентированное_программирование) [MVC](http://ru.wikipedia.org/wiki/Model–View–Controller "Model View Controller") web-фреймворк, построенный на [PHP5](http://docs.php.net/manual/ru/intro-whatis.php "PHP Hypertext Preprocessor") на общественных началах, основными целями которого являются скорость, безопасность и небольшой размер. + +[!!] Kohana использует лицензию [BSD](http://kohanaphp.com/license), так что Вы можете свободно использовать ее в любых своих открытых, коммерческих или персональных проектах. + +## Чем хороша Kohana? + +Что угодно может быть расширено с помощью уникальной [файловой системы](about.filesystem), требуется минимум [настроек](about.configuration), [перехват ошибок](debugging.errors) помогает быстро определить источник ошибок, а [отладка](debugging) и [профилирование](debugging.profiling) дают представление о происходящем внутри приложения. + +Чтобы обезопасить Ваши приложения, представлены инструменты для [защиты от XSS](security.xss), [проверки введенных данных](security.validation), работы с [cookies](security.cookies), генераторы [форм](security.forms) и [HTML](security.html). Слой [баз данных](security.database) предусматривает защиту от [SQL-инъекций](http://ru.wikipedia.org/wiki/SQL-инъекция). Разумеется, весь официальный код написан с душой и проверен на наличие уязвимостей. + +## Эта документация - г*вно! + +Мы очень активно работаем над тем, чтобы обеспечить полное документирование. В случае, если Вы не смогли найти ответ на свой вопрос, прочитайте [неофициальную wiki](http://kerkness.ca/wiki/doku.php). Если Вы хотели бы что-то добавить или изменить в Руководстве пользователя, пожалуйста [сделайте свой форк userguide'а](http://github.com/kohana/userguide), внесите свои изменения и пошлите pull request. Если Вы ещё не познакомились с работой с Git, Вы так же можете опубликовать [запрос на изменение](http://dev.kohanaframework.org/projects/kohana3/issues) (требуется регистрация). diff --git a/includes/kohana/modules/userguide/guide/ru-ru/about.mvc.md b/includes/kohana/modules/userguide/guide/ru-ru/about.mvc.md new file mode 100644 index 00000000..3d9e0d6c --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/about.mvc.md @@ -0,0 +1,5 @@ +# Модель-Представление-Контроллер + +Модель-Представление-Контроллер (сокращенно MVC) - это популярный паттерн проектирования, используя который источник данных (Модель) отделяется от шаблонов представления (Представление) и логики запроса (Контроллер). + +Это делает процесс разработки легче, т.к. облегчается повторное использование кода. Это означает, что писать придется намного меньше кода! \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/ru-ru/about.upgrading.md b/includes/kohana/modules/userguide/guide/ru-ru/about.upgrading.md new file mode 100644 index 00000000..86623ee8 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/about.upgrading.md @@ -0,0 +1,292 @@ +# Переход с 2.3.x + +Многое в Kohana v3 работает совсем по другому, нежели в Kohana 2.3, вот список самых популярных советов для обновляющихся. + +## Правила именования + +Ветка 2.x выделяет различные 'типы' классов (т.е. контроллер, модель и т.д.), используя для этого суффиксы. Директории внутри папок model / controller не влияют на имя класса. + +В 3.0 от данного подхода отказались в пользу файловых соглашений Zend framework , в котором имя класса является путем к файлу, разделенному знаками подчеркивания вместо слэщей (т.е. `/some/class/file.php` становится `Some_Class_File`) + +Смотри [описание соглашений](start.conventions) для получения подробной информации. + +## Библиотека Input + +Библиотека Input была исключена из 3.0, просто используйте `$_GET` и `$_POST`. + +### Защита от XSS + +Если Вам нужно обезопасить от XSS введенные пользователем данные, можете использовать [Security::xss_clean] : + + $_POST['description'] = security::xss_clean($_POST['description']); + +Также можно вызывать [Security::xss_clean] в качестве фильтра для объекта [Validate]: + + $validation = new Validate($_POST); + + $validate->filter('description', 'Security::xss_clean'); + +### POST & GET + +Одной из важнейших возможностей библиотеки Input было то, что если Вы попытаетесь обратиться к значению одного из суперглобальных массивов и оно не задано, библиотека Input возвратит значение по умолчанию, которое Вы зададите, например: + + $_GET = array(); + + // $id установлено в 1 + $id = Input::instance()->get('id', 1); + + $_GET['id'] = 25; + + // $id установлено в 25 + $id = Input::instance()->get('id', 1); + +В 3.0 Вы можете сделать то же самое с помощью [Arr::get]: + + $_GET = array(); + + // $id установлено в 1 + $id = Arr::get($_GET, 'id', 1); + + $_GET['id'] = 42; + + // $id установлено в 42 + $id = Arr::get($_GET, 'id', 1); + +## Библиотека ORM + +Произошло немало серьезных изменений в ORM после версии 2.3, вот список наиболее известных проблем после обновления. + +### Свойства + +Все свойства модели теперь начинаются со знака подчеркивания (_) и больше не доступны через метод `__get()`. Вместо этого Вы должны вызвать метод с именем свойства, но без подчеркивания. + +Например, свойство, бывшее в 2.3 `loaded`, теперь называется `_loaded` и извне класса доступно через `$model->loaded()`. + +### Связи + +В 2.3, если Вы хотели пройтись по связанным с моделью объектам, надо было использовать: + + foreach($model->{relation_name} as $relation) + +Однако в новой системе это не будет работать. В версии 2.3 любой запрос с использованием библиотеки Database, был сгенерирован глобально, т.е. у Вас не получится создать одновременно два запроса. Вот пример: + +# TODO: Нужен достойный пример!!!! + +Этот запрос приведет к ошибке, т.к. второй запрос будет 'наследовать' условия первого, тем самым вызывая чертовщину. +В v3.0 это было исправлено выделением каждого запроса в свою собственную 'песочницу', это приводит к тому, что некоторые вещи работают не так, как ожидается. Пример: + + foreach(ORM::factory('user', 3)->where('post_date', '>', time() - (3600 * 24))->posts as $post) + { + echo $post->title; + } + +[!!] (Смотри описание нового синтаксиса запросов в [руководстве по Database](tutorials.databases)) + +В 2.3 данный запрос вернет объект-итератор всех записей пользователя с id=3, где поле `post_date` лежит в диапазоне последних 24 часов. Однако новая версия применит условия where к пользовательской модели и вернет объединенную через join модель `Model_Post`. + +Для достижения нужного эффекта необходимо перераспределить порядок вызовов: + + foreach(ORM::factory('user', 3)->posts->where('post_date', '>', time() - (36000 * 24))->find_all() as $post) + { + echo $post->title; + } + +Аналогично со связями `has_one`: + + // Неправильно + $user = ORM::factory('post', 42)->author; + // Правильно + $user = ORM::factory('post', 42)->author->find(); + +### Связи много-ко-многим + +В 2.3 Вы можете указать `has_and_belongs_to_many` тип связи. В 3.0 эта функциональность была переработана в связь *сквозное* `has_many`. + +В моделях определяется связь `has_many` с другой моделью, но добавляется атрибут `'through' => 'table'` , где `'table'` - имя промежуточной таблицы. Например (в контексте области записи<>категории): + + $_has_many = array + ( + 'categories' => array + ( + 'model' => 'category', // внешняя модель + 'through' => 'post_categories' // промежуточная модель + ), + ); + +Если Вы настроили kohana с использованием табличных префиксов, то не стоит беспокоиться о явном указании префиксов в модели. + +### Внешние ключи + +Если Вы хотели переопределить внешний ключ в ORM ветки 2.x, надо было указать в принадлежащей (слабой) модели свойство `$foreign_keys` со значением имени внешнего ключа. + +В 3.0 Вы определяется ключ `foreign_key` в самом объявлении связи, вот так: + + Class Model_Post extends ORM + { + $_belongs_to = array + ( + 'author' => array + ( + 'model' => 'user', + 'foreign_key' => 'user_id', + ), + ); + } + +В данном примере мы настроили поле `user_id` для таблицы записей. + + + +В связях has_many атрибут `far_key` (дальний ключ) является полем в промежуточной таблице, которое связано с внешней (для текущей) таблицей, а внешний ключ является полем в промежуточной таблице, которое связано с "этой" таблицей. + +Следуя вышесказанному, "posts" связаны много-ко-многим с "categories" через `posts_sections`. + +| categories | posts_sections | posts | +|------------|------------------|---------| +| id | section_id | id | +| name | post_id | title | +| | | content | + + Class Model_Post extends ORM + { + protected $_has_many = array( + 'sections' => array( + 'model' => 'category', + 'through' => 'posts_sections', + 'far_key' => 'section_id', + ), + ); + } + + Class Model_Category extends ORM + { + protected $_has_many = array ( + 'posts' => array( + 'model' => 'post', + 'through' => 'posts_sections', + 'foreign_key' => 'section_id', + ), + ); + } + + +Очевидно, что настройки псевдонимов выглядят несколько неадекватными, но это хороший пример того, как работает схема внешний/дальний ключ. + +### ORM Iterator + +Стоит также отметить, что от класса `ORM_Iterator` отказались в пользу `Database_Result`. + +Если надо получить массив объектов ORM с ключами из первичных ключей, надо вызвать [Database_Result::as_array], вот так: + + $objects = ORM::factory('user')->find_all()->as_array('id'); + +Здесь `id` является первичным ключом таблицы. + +## Библиотека Router + +В версии 2 была библиотека Router, которая обрабатывала основной запрос приложения. Она позволяла определить базовые маршруты в файле `config/routes.php` и использовать собственные регулярные выражения для них, однако этого было недостаточно для описания чего-нибудь нестандартного. + +## Маршруты + +Система маршрутизации (теперь логичнее называть ее системой запросов) в версии 3.0 стала намного более гибкой. Маршруты объявляются в загрузочном файле (`application/bootstrap.php`) и скриптах init.php модулей (`modules/module/init.php`). (Стоит также отметить, что маршруты рассматриваются в том же порядке, в каком были определены). + +Вместо объявления массива маршрутов теперь необходимо создавать новый объект [Route] на каждый маршрут. В отличие от ветки 2.x нет необходимости отделять один URI от другого. Вместо этого Вы определяете шаблон с использованием переменных для обозначения секций (таких как контроллер, метод, id). + +К примеру, в предыдущей системе данное выражение: + + $config['([a-z]+)/?(\d+)/?([a-z]*)'] = '$1/$3/$1'; + +подменит URI `controller/id/method` на `controller/method/id`. В 3.0 следует использовать: + + Route::set('reversed','((/(/)))') + ->defaults(array('controller' => 'posts', 'action' => 'index')); + +[!!] Каждому маршруту назначается уникальное имя (в данном случае он называется `reversed`), причины описаны в [учебнике по URL](tutorials.urls). + +Угловые скобки являются признаком динамических секций, которые будут сохранены в виде переменных. Круглые скобки обозначают необязательные участки. Если Вы хотите обрабатывать только адреса, начинающиеся с `admin`, используйте: + + Rouse::set('admin', 'admin(/(/(/)))'); + +А если надо заставить пользователя указать контроллер: + + Route::set('admin', 'admin/(/(/))'); + +Также Kohana не устанавливает сама значений по умолчанию. Если Вы хотите, чтобы Kohana установила метод 'index' как дефолтный, необходимо указать это в явном виде! Сделайте это с помощью [Route::defaults]. Когда надо указать регулярное выражение для отдельных сегментов uri, добавьте параметр - массив вида `segment => regex`. Например: + + Route::set('reversed', '((/(/)))', array('id' => '[a-z_]+')) + ->defaults(array('controller' => 'posts', 'action' => 'index')) + +Значение сегмента `id` теперь должно состоять только из маленьких латинских букв и знака подчеркивания. + +### Экшены + +Еще один момент, который необходимо отметить - методы контроллера, которые должны быть доступны через url теперь называются "экшены" ("actions"), и начинаются с префикса 'action_'. В вышеуказанном примере, если пользователь введет `admin/posts/1/edit`, то экшен будет называться `edit`, но вызываемый метод контроллера получится `action_edit`. Подробности в [учебнике url](tutorials.urls). + +## Сессии + +Больше нет методов Session::set_flash(), Session::keep_flash() и Session::expire_flash(), вместо них используйте [Session::get_once]. + +## Хэлпер URL + +Изменилось немногое - `url::redirect()` перемещен в `$this->request->redirect()` (при вызове из контроллера) / `Request::instance()->redirect()` + +`url::current` заменен на `$this->request->uri()` + +## Valid / Validation + +Эти два класса были объединены в единый класс `Validate`. + +Немного изменился синтаксис для валидации массивов: + + $validate = new Validate($_POST); + + // Применяем фильтр на все элементы массива + $validate->filter(TRUE, 'trim'); + + // Для указания специфических правил используйте rule() + $validate + ->rule('field', 'not_empty') + ->rule('field', 'matches', array('another_field')); + + // Устанавливайте множество правил для одного поля через rules(), передавая массив вида rules => params в качестве второго параметра + $validate->rules('field', array( + 'not_empty' => NULL, + 'matches' => array('another_field') + )); + +Стандартное правило 'required' было переименовано в 'not_empty' для ясности. + +## Библиотека View + +Сделано несколько незначительных изменений, которые стоит отметить. + +В 2.3 представления формировались в зоне видимости контроллера, что позволяло в представлении использовать `$this` как ссылку на контроллер, и в 3.0 теперь по-другому. Представления теперь генерируются в пустом окружении. Если необходимо использовать `$this` в шаблоне, создайте ссылку с помощью [View::bind]: `$view->bind('this', $this)`. + +Тем не менее, стоит отметить, что это *очень* плохая привычка, т.к. повышает сцепление представления с контроллером и усложняет повторное использование. Рекомендуется подключать необходимые переменные таким образом: + + $view = View::factory('my/view'); + + $view->variable = $this->property; + + // ИЛИ если нравится запись по цепочке + + $view + ->set('variable', $this->property) + ->set('another_variable', 42); + + // НЕ рекомендуется + $view->bind('this', $this); + +Так как представление формируется в пустом окружении, `Controller::_kohana_load_view` теперь избыточно. Если надо изменить представление до его генерации (например, добавить меню сайта), используйте [Controller::after]. + + template->menu = '...'; + + return parent::after(); + } + } diff --git a/includes/kohana/modules/userguide/guide/ru-ru/debugging.code.md b/includes/kohana/modules/userguide/guide/ru-ru/debugging.code.md new file mode 100644 index 00000000..b4ba01d2 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/debugging.code.md @@ -0,0 +1,18 @@ +# Отладка + +Kohana включает несколько удобных инструментов для отладки Вашего приложения. + +Основной из них - [Kohana::debug]. Этот простой метод показывает любое количество переменных, аналогично [var_export] или [print_r], но с использованием HTML для дополнительного форматирования. + + // Показывает дамп переменных $foo и $bar + echo Kohana::debug($foo, $bar); + +Kohana также предоставляет метод для отображения исходного кода отдельного файла, используя [Kohana::debug_source]. + + // Показывает текущую линию исходного кода + echo Kohana::debug_source(__FILE__, __LINE__); + +Если вы хотите показать информацию о файлах приложения, не показывая реального пути, вы можете использвать [Kohana::debug_path]: + + // Показывает "APPPATH/cache" вместо реального пути + echo Kohana::debug_file(APPPATH.'cache'); diff --git a/includes/kohana/modules/userguide/guide/ru-ru/debugging.errors.md b/includes/kohana/modules/userguide/guide/ru-ru/debugging.errors.md new file mode 100644 index 00000000..7ec04b04 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/debugging.errors.md @@ -0,0 +1,22 @@ +# Обработка ошибок/исключений + +Kohana предоставляет обработчик как для исключений, так и для ошибок (он превращает ошибку в исключение с помощью стандартного PHP-класса [ErrorException](http://php.net/errorexception)). Обработчик показывает множество подробностей и внутреннее состояние приложения: + +1. Класс исключения +2. Уровень ошибки +3. Текст ошибки +4. Исходный код, вызвавший ошибку, соответствующая строка подсвечивается +5. [Трассировка](http://php.net/debug_backtrace) хода выполнения +6. Подключенные файлы, загруженные расширения и глобальные переменные + +## Пример + +Нажмите по любой ссылке для раскрытия блока дополнительной информации: + +
      {{userguide/examples/error}}
      + +## Отключение обработчика ошибок/исключений + +Если Вы не хотите использовать встроенный обработчик ошибок, отключите его с помощью [Kohana::init]: + + Kohana::init(array('errors' => FALSE)); diff --git a/includes/kohana/modules/userguide/guide/ru-ru/debugging.profiling.md b/includes/kohana/modules/userguide/guide/ru-ru/debugging.profiling.md new file mode 100644 index 00000000..020bac18 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/debugging.profiling.md @@ -0,0 +1,20 @@ +# Профилирование + +Kohana предлагает очень простой способ для отображения статистики Вашего приложения: + +1. Системных вызовов [Kohana] +2. Запросов (Requests) +3. Запросов к [базам данных](Database) +4. Среднего времени выполнения Вашего приложения + +## Пример + +Вы можете отобразить или собрать текущую статистику [профилировщика](profiler) в любое время: + +
      + +
      + +## Что получится + +{{profiler/stats}} \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/ru-ru/features.md b/includes/kohana/modules/userguide/guide/ru-ru/features.md new file mode 100644 index 00000000..76f3fa7c --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/features.md @@ -0,0 +1 @@ +Эта страница перечисляет возможности Kohana v3 \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/ru-ru/menu.md b/includes/kohana/modules/userguide/guide/ru-ru/menu.md new file mode 100644 index 00000000..d8cf8eee --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/menu.md @@ -0,0 +1,32 @@ +1. **Первые шаги** + - [Что такое Kohana?](about.kohana) + - [Соглашения и стили](about.conventions) + - [Модель-Представление-Контроллер](about.mvc) + - [Файловая система](about.filesystem) + - [Порядок выполнения](about.flow) + - [Установка](about.install) + - [Обновление](about.upgrading) + - [Обзор API](api) +2. **Основные операции** + - [Настройка](using.configuration) + - [Автозагрузка](using.autoloading) + - [Представление и HTML](using.views) + - [Сессии и Cookies](using.sessions) + - [Сообщения (Messages)](using.messages) + - [Интернационализация](using.translation) +3. **Отладка** + - [Отладка кода](debugging.code) + - [Обработка ошибок](debugging.errors) + - [Профилирование](debugging.profiling) +4. **Безопасность** + - [XSS](security.xss) + - [Валидация](security.validation) + - [Cookies](security.cookies) + - [База данных](security.database) +5. **Обучение** + - [Hello, World](tutorials.helloworld) + - [Маршруты, URL и ссылки](tutorials.urls) + - [Очистка URL, ЧПУ](tutorials.removeindex) + - [Базы данных](tutorials.databases) + - [ORM](tutorials.orm) + - [Работа с Git](tutorials.git) \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/ru-ru/security.cookies.md b/includes/kohana/modules/userguide/guide/ru-ru/security.cookies.md new file mode 100644 index 00000000..13a8480d --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/security.cookies.md @@ -0,0 +1,3 @@ +# Безопасность Cookie + +[!!] заглушка diff --git a/includes/kohana/modules/userguide/guide/ru-ru/security.database.md b/includes/kohana/modules/userguide/guide/ru-ru/security.database.md new file mode 100644 index 00000000..504a2740 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/security.database.md @@ -0,0 +1,3 @@ +# Безопасность Database + +[!!] заглушка diff --git a/includes/kohana/modules/userguide/guide/ru-ru/security.validation.md b/includes/kohana/modules/userguide/guide/ru-ru/security.validation.md new file mode 100644 index 00000000..4c0d21bb --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/security.validation.md @@ -0,0 +1,243 @@ +# Валидация + +С использованием [Validate] класса можно произвести валидацию любого массива. По ключу значения массива ("наименование поля" для валидатора), к объекту валидации можно добавить ярлыки, фильтры, правила и функции обратного вызова. + +labels (ярлыки) +: Ярлык - это удобочитаемая интерпретация имени поля. + +filters (фильтры) +: Фильтр видоизменяет значение поля перед применением к нему правил и функций обратного вызова. + +rules (правила) +: Правило - это проверка значения поля, которая возвращает `TRUE` или `FALSE` в результате проверки. Если правило возвращает `FALSE`, то полю будет добавлена информация об ошибке. + +callbacks (функция обратного вызова) +: Функция обратного вызова - это пользовательский метод, который имеет доступ до содержания объекта валидации. Возвращаемое значение функции игнорируется, ввиду этого, функция должна сама добавлять ошибку к полу объекта валидации, используя метод [Validate::error], при возникновении ошибки. + +[!!] Заметьте, что функции обратного вызова объекта [Validate] и функции обратного вызова PHP ([PHP callbacks](http://php.net/manual/language.pseudo-types.php#language.types.callback)) - это не одно и то же. + +Если вместо имени поля при добавлении фильтра, правила или функции обратного вызова использовать значение `TRUE`, то этот фильтр, правило или функция будет применена ко всем полям объекта Validate. + +**В мерах предосторожности, объект [Validate] удалит все поля из массива, которым не присвоен ярлык, фильтр, правило или функция обратного вызова. Это предотвращает доступ до полей, которые не участвуют в валидации.** + +Создание объекта валидации производится с использованием метода [Validate::factory]: + + $post = Validate::factory($_POST); + +[!!] Далее в данном руководстве будет использован объект `$post`. Как пример, будет рассмотрена валидация регистрации нового пользователя. + +### Стандартные правила + +Класс валидации содержит следующие правила: + +Наименование правила | Действие +------------------------- |------------------------------------------------- +[Validate::not_empty] | Значение не должно быть пустым +[Validate::regex] | Проверяется значение на совпадение с регулярным выражением +[Validate::min_length] | Минимальная длина значения (минимальное количество знаков) +[Validate::max_length] | Максимальная длина значения (максимальное количество знаков) +[Validate::exact_length] | Длина значения должна быть равна указанному числу +[Validate::email] | Значение должно представлять собой emal адрес +[Validate::email_domain] | Проверяет существование email домена +[Validate::url] | Значение должно представлять собой URL +[Validate::ip] | Значение должно представлять собой IP адрес +[Validate::phone] | Значение должно представлять собой номер телефона +[Validate::credit_card] | Значение должно представлять собой номер кредитной карты +[Validate::date] | Значение должно представлять собой дату (и время) +[Validate::alpha] | Допустимы только буквенные значения +[Validate::alpha_dash] | Допустимы значения, состоящие из букв и тире +[Validate::alpha_numeric] | Допустимы только буквенные и числовые значения +[Validate::digit] | Значение должно представлять собой целое число +[Validate::decimal] | Значение должно представлять собой число десятичное число или число с плавающей точкой +[Validate::numeric] | Допустимы только цифровые символы +[Validate::range] | Значение должно быть в указанных пределах +[Validate::color] | Значение должно представлять собой правильное значение цвета в HEX +[Validate::matches] | Значение должно совпадать со значением другого поля + +[!!] Любой метод [Validate] класса может быть использован как правило валидации без определения полного обратного вызова (callback). Например, добавление `'not_empty'` - то же самое, что и `array('Validate', 'not_empty')`. + +## Добавление фильтров + +Фильтр валидации задаётся как имя поля, метод или функция (используя синтаксис [PHP callback](http://ru2.php.net/callback)) и массив параметров: + + $object->filter($field, $callback, $parameters); + +Фильтры изменяют значение поля перед проверкой правилами или функциями обратного вызова. + +Для того, чтобы конвертировать значение поля "username" в нижний регистр: + + $post->filter('username', 'strtolower'); + +Если мы хотим удалить все пробелы до и после значения у *всех* полей: + + $post->filter(TRUE, 'trim'); + +## Добавление правил + +Все правила валидации задаются как имя поля, метод или функция (используя синтаксис [PHP callback](http://ru2.php.net/callback)) и массива параметров: + + $object->rule($field, $callback, $parameters); + +Для начала, инициируем валидацию `$_POST` массива, который содержит информацию о регистрации: + + $post = Validate::factory($_POST); + +Далее, нам необходимо обработать полученные данные, используя [Validate] класс. Во-первых, добавим несколько правил: + + $post + ->rule('username', 'not_empty') + ->rule('username', 'regex', array('/^[a-z_.]++$/iD')) + + ->rule('password', 'not_empty') + ->rule('password', 'min_length', array('6')) + ->rule('confirm', 'matches', array('password')) + + ->rule('use_ssl', 'not_empty'); + +В виде правила можно использовать практически все функции PHP. Например, можно проверить, верное ли значение ввёл пользователь на SSL вопрос: + + $post->rule('use_ssl', 'in_array', array(array('yes', 'no'))); + +Имейте в виду, что все параметры должны быть в виде массива! Если параметры были не в виде массива, то функция `in_array` была бы вызвана как `in_array($value, 'yes', 'no')`, что привело бы к PHP ошибке. + +Пользовательские правила могут быть добавлены, используя синтаксис [PHP callback](http://ru2.php.net/callback): + + $post->rule('username', array($model, 'unique_username')); + +Метод `$model->unique_username()` определяется следующим образом: + + public function unique_username($username) + { + // Проверка на то, имеется ли указанное значение username в БД + return ! DB::select(array(DB::expr('COUNT(username)'), 'total')) + ->from('users') + ->where('username', '=', $username) + ->execute() + ->get('total'); + } + +[!!] Пользовательские правила позволяют производить большое количество дополнительных проверок и могут быть использованы для различных целей множество раз. Зачастую, они создаются как методы модели, однако, если необходимо, могут быть определены в любом классе. + +## Добавление функций обратного вызова (callbacks) + +Все функции обратного вызова (callback) определяются как имя поля, метод или функция (используя синтаксис [PHP callback](http://ru2.php.net/callback)) и массива параметров: + + $object->callback($field, $callback, $parameters); + +[!!] До версии kohana 3.0.7, функции обратного вызова не обрабатывали входных параметров, в отличие от фильтров и правил. Если Вы используете старую версию, эти параметры будут проигнорированы. + +Пароль пользователя должен быть захэширован, если он прошёл валидацию, поэтому это можно сделать, используя функцию обратного вызова: + + $post->callback('password', array($model, 'hash_password')); + +Это подразумевает, что метод `$model->hash_password()` будет работать следующим образом: + + public function hash_password(Validate $array, $field) + { + if ($array[$field]) + { + // Хэшировать пароль, если он присутствует + $array[$field] = sha1($array[$field]); + } + } + +# Полный пример + +Во-первых, нам понадобится представление ([View]), которое содержит HTML форму и которое будет располагаться в `application/views/user/register.php`: + + + +

      Были допущены следующие ошибки:

      +
        + +
      • + + + +
        +
        +
        + +
        +
        +
        Пароль должен быть длиной как минимум в 6 знаков.
        +
        +
        + +
        +
        'Всегда', 'no' => 'По необходимости'), $post['use_ssl']) ?>
        +
        В целях безопасности, при проведении платежей всегда используется протокол SSL.
        +
        + + + + +[!!] В этом примере повсеместно используется хелпер [Form]. Использование [Form] вместо формирования HTML кода формы вручную, гарантирует корректную обработку входящих данных. Если Вы предпочитаете писать HTML собственноручно, используйте метод [HTML::chars], чтобы обезопасить приложение от введённых пользователем данных. + +Во-вторых, нам потребуется контроллер и действие этого контроллера, чтобы произвести регистрацию, которые будут располагаться в файле `application/classes/controller/user.php`: + + class Controller_User extends Controller { + + public function action_register() + { + $user = Model::factory('user'); + + $post = Validate::factory($_POST) + ->filter(TRUE, 'trim') + + ->filter('username', 'strtolower') + + ->rule('username', 'not_empty') + ->rule('username', 'regex', array('/^[a-z_.]++$/iD')) + ->rule('username', array($user, 'unique_username')) + + ->rule('password', 'not_empty') + ->rule('password', 'min_length', array('6')) + ->rule('confirm', 'matches', array('password')) + + ->rule('use_ssl', 'not_empty') + ->rule('use_ssl', 'in_array', array(array('yes', 'no'))) + + ->callback('password', array($user, 'hash_password')); + + if ($post->check()) + { + // Данные прошли проверку, теперь можно зарегистрировать пользователя + $user->register($post); + + // Всегда делайте редирект после успешного POST запроса, чтобы избежать + // повторного ввода информации при обновлении страницы + $this->request->redirect('user/profile'); + } + + // Валидация не прошла, собираем ошибки + $errors = $post->errors('user'); + + // Отображаем форму регистрации + $this->request->response = View::factory('user/register') + ->bind('post', $post) + ->bind('errors', $errors); + } + + } + +Так же нам потребуется модель пользователя, которую поместим в `application/classes/model/user.php`: + + class Model_User extends Model { + + public function register($array) + { + // Создание нового пользователя в БД + $id = DB::insert(array_keys($array)) + ->values($array) + ->execute(); + + // Сохраняем id нового пользователя в cookie + cookie::set('user', $id); + + return $id; + } + + } + +Вот и всё, у нас имеется готовый пример для регистрации пользователя, который правильно проверяет введённые пользователем данные! \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/ru-ru/security.xss.md b/includes/kohana/modules/userguide/guide/ru-ru/security.xss.md new file mode 100644 index 00000000..428c958c --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/security.xss.md @@ -0,0 +1,15 @@ +# Защита от межсайтового скриптинга (XSS) + +Первый шаг для предотвращения [XSS](http://ru.wikipedia.org/wiki/Xss) атак - необходимо знать, когда и от чего себя защищать. XSS может быть выполнен тогда, когда он отображается внутри HTML-содержимого, иногда после введения данных через форму или выборки из базы данных. Любая глобальная переменная, содержащая информацию от клиента, может быть заражена. Это касается данных из `$_GET`, `$_POST` и `$_COOKIE`. + +## Предотвращение + +Существует несколько простых правил, защищающих ваше приложение от XSS. Первое - использование метода [Security::xss] для очистки любых входящих данных, полученных из глобальных переменных. Если вы не ожидаете HTML код в переменной, используйте [strip_tags](http://php.net/strip_tags) для удаления нежелательных HTML тэгов. + +[!!] Если вы позволяете пользователям передавать HTML в ваше приложение, настоятельно рекомендуется использовать утилиты для очистки HTML кода, такие как [HTML Purifier](http://htmlpurifier.org/) или [HTML Tidy](http://php.net/tidy). + +Второй шаг - всегда очищать данные перед их вставкой в HTML код. Класс [HTML] предоставляет возможность создавать наиболее употребляемые тэги, включая ссылки на скрипты и таблицы стилей, гиперссылки, изображения, ссылки на адреса электронной почты (mailto). Все подозрительные данные должны быть экранированы с помощью [HTML::chars]. + +## Ссылки + +* [OWASP XSS Cheat Sheet](http://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet) diff --git a/includes/kohana/modules/userguide/guide/ru-ru/start.autoloading.md b/includes/kohana/modules/userguide/guide/ru-ru/start.autoloading.md new file mode 100644 index 00000000..b3ad1dc8 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/start.autoloading.md @@ -0,0 +1,17 @@ +# Автозагрузка + +Kohana использует все преимущества [автозагрузки](http://php.net/manual/language.oop5.autoload.php) в PHP. Это позволяет не использовать функций [include](http://php.net/include) или [require](http://php.net/require) перед использованием класса. + +Классы подгружаются с помощью метода [Kohana::auto_load], который использует простое соотношение имени класса с именим файла этого класса: + +1. Классы располагаются в категории `classes/` в [файловой системе](start.filesystem) фреймворка +2. Все нижние подчёркивания в имени класса конвертируются в слеши +2. Имя файла пишется в нижнем регистре + +При вызове ещё не подгружённого класса (например, `Session_Cookie`), Kohana будет искать с помощью [Kohana::find_file] файл `classes/session/cookie.php`. + +## Пользовательские автозагрузчики + +[!!] Автозагрузчик по-умолчанию активирован в `application/bootstrap.php`. + +Дополнительные загрузчики классов могут быть добавлены с использованием [spl_autoload_register](http://php.net/spl_autoload_register). diff --git a/includes/kohana/modules/userguide/guide/ru-ru/start.configuration.md b/includes/kohana/modules/userguide/guide/ru-ru/start.configuration.md new file mode 100644 index 00000000..45ccc566 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/start.configuration.md @@ -0,0 +1,92 @@ +# Общие настройки + +[!!] TODO: описание преимуществ статических свойств конфигурации + +## Настройки ядра + +Первой задачей конфигурирования новой установки Kohana является изменение параметров [Kohana::init] в `application/bootstrap.php`. Вот эти параметры: + +`boolean` errors +: Использование встроенного обработчика ошибок и исключений. (По-умолчанию: `TRUE`) Установите в `FALSE`, чтобы отменить перехват ошибок и исключений фреймворком. + +`boolean` profile +: Использовать возможности встроенного бенчмаркинга. (По-умолчанию: `TRUE`) Установите в `FALSE`, чтобы отменить профилирование. Для увеличения производительности на стадии production данный параметр рекомендуется отключать. + +`boolean` caching +: Кэширование информации о расположении файлов между запросами. (По-умолчанию: `FALSE`) Установите в `TRUE`, чтобы включить кэширование значений абсолютных путей исполняемых файлов. Это значительно ускоряет [Кохана::find_file] и может иногда оказать серьезное влияние на производительность. Используйте на этапе production или для тестирования. + +`string` charset +: Кодировка, используемая во всех операциях ввода-вывода. (По-умолчанию: `"utf-8"`) Должна поддерживаться как [htmlspecialchars](http://php.net/htmlspecialchars), так и [iconv](http://php.net/iconv). + +`string` base_url +: Базовый URL для приложения. (По-умолчанию: `"/"`) Может быть как абсолютным, так и относительным URL. Например, "http://example.com/kohana/" или просто "/kohana/": подходят оба варианта. + +`string` index_file +: Имя PHP файла, который запускает приложение (фронтенд). (По-умолчанию: `"index.php"`) Установите в `FALSE`, если намереваетесь использовать URL rewriting. + +`string` cache_dir +: Директория для хранения файлового кэша. (По-умолчанию: `"application/cache"`) Директория должна быть **доступна для записи**. + +## Настройки Cookie + +Перед запуском production-версии сайта следует установить значения некоторых статических свойств [Cookie] класса. + +`string` salt +: Уникальная строка salt-значения, которая используется для работы [cookies](security.cookies) + +`integer` expiration +: По-умолчанию: время жизни cookies в секундах + +`string` path +: URL, ограничивающий доступ к cookies + +`string` domain +: Домен, ограничивающий доступ к cookies + +`boolean` secure +: Позволить использовать cookies только по протоколу HTTPS + +`boolean` httponly +: Позволить использовать cookies только по протоколу HTTP (также закрывается доступ через Javascript) + +# Конфигурационные файлы + +Настройки хранятся в виде PHP файлов примерно такого вида: + +~~~ + 'value', + 'options' => array( + 'foo' => 'bar', + ), +); +~~~ + +Если конфигурационный файл был назван `myconf.php`, то для доступа к нему можно использовать следующий код: + +~~~ +$config = Kohana::config('myconf'); +$options = $config['options']; +~~~ + +[Kohana::config] позволяет так же использовать "пути с точкой" для доступа к отдельным ключам конфигурационного массива. + +Для получения массива "options": + +~~~ +$options = Kohana::config('myconf.options'); +~~~ + +Для получения значения ключа "foo" массива "options": + +~~~ +$foo = Kohana::config('myconf.options.foo'); +~~~ + +Вы можете работать с настроечными массивами как с объектами, если это будет удобнее: + +~~~ +$options = Kohana::config('myconf')->options; +~~~ \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/ru-ru/start.controllers.md b/includes/kohana/modules/userguide/guide/ru-ru/start.controllers.md new file mode 100644 index 00000000..189c7390 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/start.controllers.md @@ -0,0 +1,104 @@ +# Контроллеры + +Контроллеры являются промежуточным звеном между моделью и представлением. Они передают информацию в модель для изменения данных, и запрашивают информацию от модели. Например, операции работы с базой данных: вставка (insert), изменение (update) и удаление (delete) как операции редактирования данных, и выборка (select) для извлечения данных. Информацию, полученную от модели, контроллеры перенаправляют в представления, которые содержат конечный результат, предназначенный для отображения пользователям. + +Контроллеры вызываются с помощью URL. За более подробной информацией обратитесь к разделу [URL и ссылки](start.urls). + + + +## Название контроллера и его содержание + +Имя класса контроллера должно соответствовать имени файла. + +**Соглашения при использовании контроллеров** + +* имя файла должно быть в нижнем регистре, например: `articles.php` +* файл контроллера должен располагаться в (под-)директории **classes/controller**, например: `classes/controller/articles.php` +* имя класса контроллера должно соответствовать имени файла, начинаться с заглавной буквы и должно начинаться с префикса **Controller_**, например: `Controller_Articles` +* класс контроллера должен быть потомком класса Controller. +* методы контроллера, предназначенные для вызова через URI, должны начинаться с префикса **action_** (например: `action_do_something()` ) + + + +### Пример простейшего контроллера + +Создадим простой контроллер, который будет выводить на экран 'Hello World!'. + +**application/classes/controller/article.php** +~~~ + MODPATH.'database', + ... + )); + +## Настройка {#configuration} + +После подключения модуля необходимо создать файл настроек, чтобы модуль знал как соединиться с базой данных. Пример конфигурационного файла можно найти в `modules/database/config/database.php`. + +Структура группы настроек базы данных ("instance") выглядит следующим образом: + + string INSTANCE_NAME => array( + 'type' => string DATABASE_TYPE, + 'connection' => array CONNECTION_ARRAY, + 'table_prefix' => string TABLE_PREFIX, + 'charset' => string CHARACTER_SET, + 'profiling' => boolean QUERY_PROFILING, + ), + +[!!] В одном конфигурационном файле можно определить несколько таких групп. + +Очень важно понимать каждый параметр конфигурации. + +INSTANCE_NAME +: Соединения могут быть названы как Вы захотите, но одно из них обязательно должно называться "default" (группа по умолчанию). + +DATABASE_TYPE +: Один из установленных драйверов баз данных. Kohana поставляется с драйверами "mysql" и "pdo". + +CONNECTION_ARRAY +: Специфические настройки драйвера для соединения с БД. (Настройки драйвера описаны [ниже](#connection_settings).) + +TABLE_PREFIX +: Префикс, который будет добавлен к названиям таблиц классом [query builder](#query_building). + +QUERY_PROFILING +: Включает [профилирование](debugging.profiling) запросов к БД. + +### Пример + +Ниже описаны два соединения с MySQL, одно локальное, а другое удаленное. + + return array + ( + 'default' => array + ( + 'type' => 'mysql', + 'connection' => array( + 'hostname' => 'localhost', + 'username' => 'dbuser', + 'password' => 'mypassword', + 'persistent' => FALSE, + 'database' => 'my_db_name', + ), + 'table_prefix' => '', + 'charset' => 'utf8', + 'profiling' => TRUE, + ), + 'remote' => array( + 'type' => 'mysql', + 'connection' => array( + 'hostname' => '55.55.55.55', + 'username' => 'remote_user', + 'password' => 'mypassword', + 'persistent' => FALSE, + 'database' => 'my_remote_db_name', + ), + 'table_prefix' => '', + 'charset' => 'utf8', + 'profiling' => TRUE, + ), + ); + +### Настройки соединения {#connection_settings} + +Каждый драйвер БД имеет свои настройки соединения. + +#### MySQL + +БД MySQL поддерживает следующие параметры массива `connection`: + +Тип | Параметр | Описание | Значение по умолчанию +----------|------------|-----------------------------| ------------------------- +`string` | hostname | Имя сервера или IP-адрес | `localhost` +`integer` | port | Номер порта | `NULL` +`string` | socket | сокет UNIX | `NULL` +`string` | username | Имя пользователя | `NULL` +`string` | password | Пароль | `NULL` +`boolean` | persistent | Постоянное соединение | `FALSE` +`string` | database | Имя базы данных (схемы) | `kohana` + +#### PDO + +База данных PDO database принимает следующие опции массива `connection`: + +Тип | Параметр | Описание | Значение по умолчанию +----------|------------|-----------------------------| ------------------------- +`string` | dsn | Идентификатор источника PDO | `localhost` +`string` | username | Имя пользователя | `NULL` +`string` | password | Пароль | `NULL` +`boolean` | persistent | Постоянное соединение | `FALSE` + +!! Если Вы используете PDO и не уверены, что прописывать в параметре `dsn`, ознакомьтесь с [PDO::__construct](http://php.net/pdo.construct). + +## Соединения и сущности {#connections} + +Каждая группа настроек связана с экземпляром базы данных ("сущность"). Каждая сущность может быть получена через вызов [Database::instance]: + + $default = Database::instance(); + $remote = Database::instance('remote'); + +Чтобы порвать соединение с базой данных, просто уничтожьте объект: + + unset($default, Database::$instances['default']); + +Если Вы хотите разорвать соединения со всеми сущностями за раз: + + Database::$instances = array(); + +## Создаем запросы {#making_queries} + +Существует два способа создать запросы. Простейший путь - использование [Database_Query] для создания запросов, через [DB::query]. Эти запросы называются "подготовленные выражения" и позволяют устанавливать параметры, которые автоматически экранируются. Второй путь - построение через специальные методы. Это возможно с помощью объекта [query builder](#query_building). + +[!!] Все запросы выполняются методом `execute`, который принимает объект [Database] или имя сущности. Смотри [Database_Query::execute]. + +### Подготовленные выражения + +Подготовленные выражения позволяют писать SQL-запросы вручную, в то же время значения будут автоматически экранированы, чтобы избежать [SQL-инъекций](http://wikipedia.org/wiki/SQL_Injection). Создать запрос просто: + + $query = DB::query(Database::SELECT, 'SELECT * FROM users WHERE username = :user'); + +Фабричный метод [DB::query] создает новый класс [Database_Query] и возвращает его, поддерживая цепочки вызовов. Запрос содержит параметр `:user`, которому будет назначено значение: + + $query->param(':user', 'john'); + +[!!] Имя параметра может быть любой строкой, в дальнейшем оно будет заменено функцией [strtr](http://php.net/strtr). Рекомендуется **не** использовать знак доллара в составе имени параметра во избежание путаницы. + +Если Вы хотите увидеть SQL, предназначенный для выполнения, просто преобразуйте объект в строку: + + echo Kohana::debug((string) $query); + // выведет: + // SELECT * FROM users WHERE username = 'john' + +Также Вы можете изменить параметр `:user`, снова вызвав [Database_Query::param]: + + $query->param(':user', $_GET['search']); + +[!!] Для задания нескольких параметров за раз используйте [Database_Query::parameters]. + +После установки всех параметров можно выполнить запрос: + + $query->execute(); + +Также допустимо привязать параметр к переменной, используя [ссылки на переменные]((http://php.net/language.references.whatdo)). Это может стать очень полезным при многократном выполнении схожих запросов: + + $query = DB::query(Database::INSERT, 'INSERT INTO users (username, password) VALUES (:user, :pass)') + ->bind(':user', $username) + ->bind(':pass', $password); + + foreach ($new_users as $username => $password) + { + $query->execute(); + } + +В данном примере переменные `$username` и `$password` меняются в каждой итерации цикла `foreach`. Когда переменные меняются, также изменяют значение и параметры запроса `:user` и `:pass`. Правильное и уместное использование привязки параметров может сделать код более компактным. + +### Конструктор запросов {#query_building} + +Динамическое создание запросов с использованием объектов и методов позволяет писать запросы очень быстро и предсказуемо. Построитель запросов также заключает в кавычки идентификаторы (имена таблиц и полей), также как и экранирует значения. + +[!!] На данный момент невозможно комбинировать построитель запросов с подготовленными выражениями. + +#### Выборка (SELECT) + +Каждый тип запросов представлен отдельным классом со своими методами. К примеру, чтобы создать запрос типа SELECT, используем [DB::select]: + + $query = DB::select()->from('users')->where('username', '=', 'john'); + +По умолчанию, [DB::select] будет запрашивать все поля (`SELECT * ...`), но можно указать, какие столбцы извлекать: + + $query = DB::select('username', 'password')->from('users')->where('username', '=', 'john'); + +А теперь посмотрим, к чему привела эта цепочка вызовов. Сперва мы создаем новый объект выборки методом [DB::select]. Далее, устанавливаем таблицу(ы) с помощью метода `from`. И напоследок, ищем конкретные записи через метод `where`. Можно посмотреть генерируемый код SQL просто преобразовывая объект к строке: + + echo Kohana::debug((string) $query); + // Покажет: + // SELECT `username`, `password` FROM `users` WHERE `username` = 'john' + +Обратили внимание, что имена полей и таблиц автоматически экранированы, также как и значения? Это одно из ключевых преимуществ использования построителя запросов. + +Также допустимо создавать псевдонимы `AS` для выборки: + + $query = DB::select(array('username', 'u'), array('password', 'p'))->from('users'); + +Сгенерируется следующий SQL-запрос: + + SELECT `username` AS `u`, `password` AS `p` FROM `users` + +#### Вставка (INSERT) + +Чтобы создать записи в базе данных, используйте [DB::insert], создающий запросы INSERT: + + $query = DB::insert('users', array('username', 'password'))->values(array('fred', 'p@5sW0Rd')); + +Запрос сформирует код: + + INSERT INTO `users` (`username`, `password`) VALUES ('fred', 'p@5sW0Rd') + +#### Обновление (UPDATE) + +Для редактирования существующей записи предназначен метод [DB::update], он возвращает запрос UPDATE: + + $query = DB::update('users')->set(array('username' => 'jane'))->where('username', '=', 'john'); + +В результате получим запрос: + + UPDATE `users` SET `username` = 'jane' WHERE `username` = 'john' + +#### Удаление (DELETE) + +Для удаления записи используется [DB::delete], он создает запрос DELETE: + + $query = DB::delete('users')->where('username', 'IN', array('john', 'jane')); + +Получим следующий запрос: + + DELETE FROM `users` WHERE `username` IN ('john', 'jane') + +#### Функции работы с базами данных {#database_functions} + +Иногда Вы можете столкнуться с ситуацией, когда надо вызвать `COUNT` или другую функцию СУБД внутри запроса. Построитель запросов позволяет использовать эти функции двумя способами. Первый - применение кавычек внутри псевдонимов: + + $query = DB::select(array('COUNT("username")', 'total_users'))->from('users'); + +Это выглядит почти также, как и стандартные псевдонимы, но имя поля обрамлено двойными кавычками. Каждый раз, когда значение в двойных кавычках обнаруживается внутри имени столбца, **только** часть внутри этих кавычек будет экранирована. Сгенерируется код SQL: + + SELECT COUNT(`username`) AS `total_users` FROM `users` + +#### Сложные выражения + +"Закавыченные" псевдонимы могут решить многие проблемы, но время от времени может понадобиться сложное выражение. В таких случаях надо использовать выражения, создаваемые методом [DB::expr]. Выражение используется для прямого вывода, экранирование не происходит. + diff --git a/includes/kohana/modules/userguide/guide/ru-ru/tutorials.git.md b/includes/kohana/modules/userguide/guide/ru-ru/tutorials.git.md new file mode 100644 index 00000000..4d158f10 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/tutorials.git.md @@ -0,0 +1,149 @@ +# Работа с Git + +Kohana применяет [git](http://git-scm.com/) для контроля версий и [github](http://github.com/kohana) для совместной разработки. Данная статья покажет Вам как использовать git и github для создания простейшего приложения. + +## Установка и настройка Git на Вашей машине + +### Установка Git + +- для OSX: [Git-OSX](http://code.google.com/p/git-osx-installer/) +- для Windows: [Msygit](http://code.google.com/p/msysgit/) +- Или загрузите git с [официального сайта](http://git-scm.com/) и установите его самостоятельно (подробности установки смотрите на сайт Git) + +### Основные глобальные настройки + + git config --global user.name "Your Name" + git config --global user.email "youremail@website.com" + +### Дополнительные, но предпочтимые настройки + +Для лучшей визуализации команд и репозиторий в командной строке, используйте следующее: + + git config --global color.diff auto + git config --global color.status auto + git config --global color.branch auto + +### Настройка автозавершения + +[!!] Следующие строки применимы только для OSX машин + +Эти строки сделают всю грязную работу за вас и после этого Вы сможете спокойно работать с git-окружением, используя автозавершение команд: + + cd /tmp + git clone git://git.kernel.org/pub/scm/git/git.git + cd git + git checkout v`git --version | awk '{print $3}'` + cp contrib/completion/git-completion.bash ~/.git-completion.bash + cd ~ + rm -rf /tmp/git + echo -e "source ~/.git-completion.bash" >> .profile + +### Всегда используйте LF в окончаниях строк + +Это соглашение, которое было принято Kohana сообществом. Выставьте эту настройку во имя Вашего Господа, особенно если хотите участвовать в kohana коммьюнити! + + git config --global core.autocrlf input + git config --global core.savecrlf true + +[!!] Более подробную информацию об окончаниях строк читайте на [GitHub'е](http://help.github.com/dealing-with-lineendings/) + +### Информация для размышления + +- [Git скринкасты](http://www.gitcasts.com/) +- [Git справочник](http://gitref.org/) +- [Pro Git book](http://progit.org/book/) + +## Основная структура + +[!!] Предполагается, что Ваш web-сервер уже настроен, и Вы будете использовать адрес для нового приложения. + +Откройте консоль, перейдите в пустую директорию `gitorial` и выполните команду `git init`. Она создаст заготовку под новый git-репозиторий. + +Далее, мы создадим подпроект ([submodule](http://www.kernel.org/pub/software/scm/git/docs/git-submodule.html)) для директории `system`. Откройте и скопируйте значение Clone URL: + +![Github Clone URL](http://img.skitch.com/20091019-rud5mmqbf776jwua6hx9nm1n.png) + +Используйте скопированный URL для создания подпроекта `system`: + + git submodule add git://github.com/kohana/core.git system + +[!!] Будет создана связь с текущей разрабатываемой версией следующего стабильного релиза. Эта версия должна практически всегда быть безопасна для использования, иметь тот же API, что в текущем стабильном релизе с исправленными ошибками. + +Теперь добавьте остальные необходимые подпроекты. Например, если нужен модуль [Database](http://github.com/kohana/database): + + git submodule add git://github.com/kohana/database.git modules/database + +После добавления модули должны быть проиниализированы: + + git submodule init + +Теперь мы должны зафиксировать текущее состояние: + + git commit -m 'Added initial submodules' + +Следующий шаг - создание структуры папок для приложения. Вот необходимый минимум: + + mkdir -p application/classes/{controller,model} + mkdir -p application/{config,views} + mkdir -m 0777 -p application/{cache,logs} + +Если запустить команду `find application`, Вы должны увидеть такой список: + + application + application/cache + application/config + application/classes + application/classes/controller + application/classes/model + application/logs + application/views + +Мы не хотим, чтобы git обрабатывал логи или файлы кэша, поэтому добавим файл `.gitignore` в соответствуюшие директории logs и cache. Теперь все нескрытые (non-hidden) файлы будут проигнорированы git'ом: + + echo '[^.]*' > application/{logs,cache}/.gitignore + +[!!] Git пропускает пустые папки, так что добавляя файл `.gitignore`, мы дополнительно заставляем git учитывать данную директорию, но не файлы внутри нее. + +Теперь загружаем файлы `index.php` и `bootstrap.php`: + + wget http://github.com/kohana/kohana/raw/master/index.php + wget http://github.com/kohana/kohana/raw/master/application/bootstrap.php -O application/bootstrap.php + +Фиксируем эти изменения: + + git add application + git commit -m 'Added initial directory structure' + +Это все необходимые изменения. Теперь у Вас имеется приложение, использующее Git для контроля версий. + +## Обновление подмодулей + +Скорее всего, на определенном этапе Вы захотите обновить свои подпроекты. Чтобы обновить все модули до последних версий `HEAD`, введите: + + git submodule foreach 'git checkout master && git pull origin master' + +Для синхронизации подпроекта `system` выполните: + + cd system + git checkout master + git pull + cd .. + git add system + git commit -m 'Updated system directory' + +Обновление отдельного модуля до определенной ревизии: + + cd modules/database + git fetch + git checkout fbfdea919028b951c23c3d99d2bc1f5bbeda0c0b + cd ../.. + git add database + git commit -m 'Updated database module' + +Заметьте, что можно так же загрузить коммит по официальной метке релиза. Например: + + git checkout 3.0.7 + +Для того, чтобы увидеть все метки, просто запустите `git tag` без дополнительных аргументов. + +Вот и всё! \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/ru-ru/tutorials.helloworld.md b/includes/kohana/modules/userguide/guide/ru-ru/tutorials.helloworld.md new file mode 100644 index 00000000..48d98119 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/tutorials.helloworld.md @@ -0,0 +1,105 @@ +# Hello, World + +Каждый фреймворк располагает примером написания приложения hello world, так что не будем нарушать традицию! + +Мы начнем с создания простейшего hello world, а затем расширим его согласно принципам MVC. + +## Основа + +Сперва надо создать контроллер, который Kohana будет использовать для обработки запроса + +Создайте файл `application/classes/controller/hello.php` в директории application и вставьте туда такой текст: + + template->message = 'hello, world!'; + } + } + +`extends Controller_Template` +: Теперь мы расширяем шаблонный контроллер (template controller), что делает работу контроллера с представлениями более удобной. + +`public $template = 'site';` +: Шаблонный контроллер должен знать, какое представление использовать. Он автоматически загрузит указанное представление в данную переменную в виде объекта. + +`$this->template->message = 'hello, world!';` +: `$this->template` является ссылкой на наш шаблон. Мы присваиваем переменной "message" значение "hello, world!", и добавляем ее в шаблон template. + +А теперь попробуем выполнить наш код... + +
        {{userguide/examples/hello_world_error}}
        + +По каким-то причинам kohana генерирует ошибку и не хочет показать наше восхитительное сообщение. + +Если мы посмотрим на сообщение с ошибкой, то увидим, что библиотека View не смогла найти наш главный шаблон, скорее всего потому что мы его еще не создали - *черт*! + +Давайте добавим файл представления `application/views/site.php` для нашего сообщения: + + + + We've got a message for you! + + + +

        +

        We just wanted to say it! :)

        + + + +Если обновить страницу, то мы увидим увидим результаты наших усилий: + +![hello, world! Мы просто хотели это произнести!](img/hello_world_2.png "hello, world! Мы просто хотели это произнести!") + +## Этап 3 - Итого! + +В данной статье Вы изучили, как создать контроллер и использовать шаблоны для отделения логики от представления. + +Очевидно, что это было всего-навсего упрощенное вступление, и оно не отражает даже малой части всех возможностей, доступных при разработке приложений с помощью kohana. diff --git a/includes/kohana/modules/userguide/guide/ru-ru/tutorials.orm.md b/includes/kohana/modules/userguide/guide/ru-ru/tutorials.orm.md new file mode 100644 index 00000000..14d2096e --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/tutorials.orm.md @@ -0,0 +1,312 @@ +# ORM {#top} + +Kohana 3.0 включает мощный модуль ORM, который использует паттерн Active Record и автоопределение информации о списке полей БД модели. + +Модуль ORM включен в дистрибутив Kohana 3.0, но нуждается в подключении перед его использованием. В файле `application/bootstrap.php` отредактируйте вызов [Kohana::modules] и добавьте модуль ORM: + + Kohana::modules(array( + ... + 'orm' => MODPATH.'orm', + ... + )); + +## Настройка {#configuration} + +ORM требует небольшую настройку перед использованием. Наследуйте Вашу модуль от ORM: + + class Model_User extends ORM + { + ... + } + +В примере выше, модель будет искать таблицу `users` в БД по умолчанию. + +### Свойства модели, отвечающие за конфигурацию + +Следующие свойства используются для настройки каждой модели: + +Тип | Название | Описание | Значение по умолчанию +----------|---------------------|-------------------------------------|--------------------------------- +`string` | _table_name | Используемая таблица БД | `имя модели в единственном числе` +`string` | _db | Название БД | `default` +`string` | _primary_key | Поле - первичный ключ | `id` +`string` | _primary_val | Титульное поле | `name` +`bool` | _table_names_plural | Имя таблицы во множественном числе | `TRUE` +`array` | _sorting | Сортировка (столбец => направление) | `primary key => ASC` +`string` | _foreign_key_suffix | Суффикс внешнего ключа | `_id` + +## Использование ORM + +### Загрузка записи + +Для создания экземпляра модели используйте метод [ORM::factory] или конструктор [ORM::__construct]: + + $user = ORM::factory('user'); + // или + $user = new Model_User(); + +Конструктор и фабричный метод также поддерживают значение первичного ключа для загрузки конкретной записи: + + // Загружаем пользователя с ID 5 + $user = ORM::factory('user', 5); + + // Проверяем успешность загрузки объекта пользователя + if ($user->loaded()) { ... } + +Опционально, Вы можете передать массив с парами ключ => значение для загрузки данных объекта по совпадающим критериям, указанным в массиве: + + // Загрузка пользователя с email joe@example.com + $user = ORM::factory('user', array('email' => 'joe@example.com')); + +### Поиск записи + +ORM поддерживает большинство методов класса [Database] для полноценного поиска данных модели. В свойстве `_db_methods` перечислен полный список поддерживаемых методов. Записи извлекаются после вызовов [ORM::find] или [ORM::find_all]. + + // Извлекаем первого активного пользователя по имени Bob + $user = ORM::factory('user') + ->where('active', '=', TRUE) + ->where('name', '=', 'Bob') + ->find(); + + // Ищем всех активных пользователей по имени Bob + $users = ORM::factory('user') + ... + ->find_all(); + +Когда Вы запрашиваете список моделей через [ORM::find_all], перебирать его можно аналогично обычным выборкам из БД: + + foreach ($users as $user) + { + ... + } + +Мощным инструментом ORM является метод [ORM::as_array], который возвращает полученные записи в виде массива. При использовании совместно с [ORM::find_all], будет возвращён массив всех записей. Хороший пример использование этого метода - когда необходимо передать значения для выпадающего списка: + + // Отображается выпадающий список пользователей + // (используется id в качестве значения select option) + form::select('user', ORM::factory('user')->find_all()->as_array('id', 'username') ... + +### Подсчёт записей + +Для получения количества записей для данного запроса, используйте [ORM::count_all]. + + // Число активных пользователей + $count = ORM::factory('user')->where('active', '=', TRUE)->count_all(); + +Если требуется подсчитать общее количество пользователей для данного запроса при лимитировании количества возвращаемых записей, используйте метод [ORM::reset] с параметром `FALSE` перед использованием `count_all`: + + $user = ORM::factory('user'); + + // Общее число пользователей (reset FALSE предотвращает объект от очистки перез запросом) + $count = $user->where('active', '=', TRUE)->reset(FALSE)->count_all(); + + // Получаем только первые 10 результатов + $users = $user->limit(10)->find_all(); + +### Доступ к свойствам модели + +Все свойства модели доступны через "магические" методы `__get` и `__set`. + + $user = ORM::factory('user', 5); + + // Выводит имя пользователя + echo $user->name; + + // Изменяет имя пользователя + $user->name = 'Bob'; + +Для хранения данных/свойств, которые отсутствуют в таблице модели, надо использовать атрибут `_ignored_columns`. + + class Model_User extends ORM + { + ... + protected $_ignored_columns = array('field1', 'field2', ...) + ... + } + +Множественные пары ключ => значение могут быть заданы с использованием метода [ORM::values]: + + $user->values(array('username' => 'Joe', 'password' => 'bob')); + +### Создаем и сохраняем записи + +Метод [ORM::save] используется как для создания новых записей, так и для обновления существующих. + + // Создаем запись + $user = ORM::factory('user'); + $user->name = 'New user'; + $user->save(); + + // Редактируем запись + $user = ORM::factory('user', 5); + $user->name = 'User 2'; + $user->save(); + + // Проверяем, сохранена ли запись + if ($user->saved()) { ... } + +Вы можете обновить множество записей с помощью метода [ORM::save_all]: + + $user = ORM::factory('user'); + $user->name = 'Bob'; + + // Все активные пользователи получат имя 'Bob' + $user->where('active', '=', TRUE)->save_all(); + +#### Использование `Updated` и `Created` для столбцов БД + +Свойства `_updated_column` и `_created_column` позволяют производить автоматическое обновление модели при её обновлении и сохранении. Это используется не по-умолчанию. Чтобы использовать эти свойства, следует их указать: + + // date_created является столбцом таблицы, в котором хранится дата создания. + // Для сохранения метки времени, используем TRUE + protected $_created_column = array('date_created' => TRUE); + + // date_modified является столбцом таблицы, в котором хранится дата изменения. + // В этом случае используется строка, определяющая формат для функции date() + protected $_updated_column = array('date_modified' => 'm/d/Y'); + +### Удаление записей + +Записи удаляются методами [ORM::delete] и [ORM::delete_all]. Эти методы работают аналогично описанному выше сохранению, за исключением того, что [ORM::delete] принимает необязательный параметр `id` для удаляемой записи. + +### Отношения + +ORM предоставляет мощную поддержку отношений таблиц. Прочитать про отношения можно в [справочнике по Ruby](http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html) + +#### Belongs-To и Has-Many + +Допустим, мы работаем со школой, которая имеет много учеников (has many). Каждый студент приписан к одной школе (принадлежит - belongs to). Необходимо определить отношения моделей следующим образом: + + // В модели school + protected $_has_many = array('students' => array()); + + // В модели student + protected $_belongs_to = array('school' => array()); + +Получаем информацию о школе студента: + + $school = $student->school; + +Ищем всех студентов школы: + + // Учтите, что после students следует вызвать метод find_all + $students = $school->students->find_all(); + + // Чтобы сузить результаты поиска: + $students = $school->students->where('active', '=', TRUE)->find_all(); + +По-умолчанию, ORM будет искать поле `school_id` в таблице модели student. Это можно изменить, используя аттрибут `foreign_key`: + + protected $_belongs_to = array('school' => array('foreign_key' => 'schoolID')); + +Внешний ключ будет перегружен как в модели student, так и в school. + +#### Has-One + +Has-One - это частный случай Has-Many, единственным отличаем которого является то, что в отношении участвует только одна запись. В дополнении к приведённому выше примеру школы, каждая школа будет иметь (has-one) только одного директора, который принадлежит (belongs-to) школе. + + // Inside the school model + protected $_has_one = array('principal' => array()); + +Как и для Belongs-To, Вам не нужно использовать метод `find` для получение объекта, ссылающегося на Has-One объект - это будет сделано автоматически. + +#### Has-Many "через" (through) + +Отношение Has-Many "through" (так же известное как Has-And-Belongs-To-Many) оспользуется в случае если объект связан с несколькими объектами другого типа и наоборот. Например, студент записан на многие занятия и на занятие ходит много студентов. В этом случаеи используется `промежуточная` таблица. Используем для нашего примера промежуточную таблицу и модель - журнал (`enrollment`). + + // В модели student + protected $_has_many = array('classes' => array('through' => 'enrollment')); + + // В модели class + protected $_has_many = array('students' => array('through' => 'enrollment')); + +Таблица enrollment должна содержать 2 внешних ключа: для занятий `class_id` и для студентов `student_id`. Наименование внешних и дальних ключей (`foreign_key` и `far_key`) могут быть переопределены при определении отношений. Например: + + // В модели student (внешний ключ ссылается на модель student, + // в то время, как дальний ключ - на class) + protected $_has_many = array( + 'classes' => array( + 'through' => 'enrollment', + 'foreign_key' => 'studentID', + 'far_key' => 'classID' + )); + + // В модели class + protected $_has_many = array( + 'students' => array( + 'through' => 'enrollment', + 'foreign_key' => 'classID', + 'far_key' => 'studentID' + )); + +Определяем в модели enrollment: + + // Журнал принадлежит как студенту, так и занятию + protected $_belongs_to = array('student' => array(), 'class' => array()); + +Для доступа к связанным объектам: + + // Для получение занятий студента + $student->classes->find_all(); + + // Для получения студентов, записанных на занятие + $class->students->find_all(); + +### Валидация + +ORM тесно взимодействует с [Validate] библиотекой, позволяя использовать возможности этого класса в следующих свойствах: + +* _rules +* _callbacks +* _filters +* _labels + +#### `_rules` + + protected $_rules = array + ( + 'username' => array('not_empty' => array()), + 'email' => array('not_empty' => array(), 'email' => array()), + ); + +`username` будет проверяться на то, что значение этого поля не является пустым. `email` поле будет проверено на соответствие значения валидному email адресу. Ввиду возможности передачи дополнительных опций для правил, значения правил задаются как пустые массивы. + +#### `_callbacks` + + protected $_callbacks = array + ( + 'username' => array('username_unique'), + ); + +Значение поля `username` будет передано методы `username_unique`. Если метод не существует в текущей модели, то будет вызвана глобальная функция. Вот пример описания этого метода: + + public function username_unique(Validate $data, $field) + { + // Логика, проверяющая уникальность имени пользователя + ... + } + +#### `_filters` + + protected $_filters = array + ( + TRUE => array('trim' => array()), + 'username' => array('stripslashes' => array()), + ); + +`TRUE` Указывает на то, что фильтр `trim` будет применён ко всем полям. Значение поля `username` будет отфильтровано с помощью функции `stripslashes` перед процессом валидации. Ввиду возможности передачи дополнительных опций для фильтров, значения фильтра задаются как пустые массивы. + +#### Проверка объекта + +Для проверки объекта, используйте [ORM::check]: + + // Задание значений объекта и дальнейшая их валидация + if ($user->values($_POST)->check()) + { + $user->save(); + } + +Для доступа к объекту валидации данной модели, можно использовать метод `validate()`: + + // Ручное добавление дополнительного фильтра + $user->validate()->filter('username', 'trim'); \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/ru-ru/tutorials.removeindex.md b/includes/kohana/modules/userguide/guide/ru-ru/tutorials.removeindex.md new file mode 100644 index 00000000..96789c90 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/tutorials.removeindex.md @@ -0,0 +1,88 @@ +# Удаление из URL `index.php` + +Для чистоты URL, Вам наверняка захочется иметь доступ до разделов Вашего приложения без `/index.php/` в адресной строке. Для этого необходимо выполнить 2 действия. + +1. Откорректировать bootstrap файл +2. Установить возможности rewriting'а на Вашем веб-сервере + +# Конфигурирование Bootstrap + +Первое, что следует сделать - это изменить значение `index_file` в [Kohana::init]: + + Kohana::init(array( + 'base_url' => '/myapp/', + 'index_file' => FALSE, + )); + +Теперь все ссылки, генерируемые методами [URL::site], [URL::base], и [HTML::anchor] не будут использовать "index.php" при построении URL. Все генерируемые ссылки будут начинаться с `/myapp/` вместо `/myapp/index.php/`. + +# URL Rewriting + +В зависимости от Вашего сервера, rewriting активируется по разному. + +## Apache + +Переименуйте `example.htaccess` в `.htaccess` и измените следующую строчку кода: + + RewriteBase /kohana/ + +RewriteBase должен совпадать со значением, указанным у Вас в `base_url` свойстве [Kohana::init]: + + RewriteBase /myapp/ + +В большинстве случаев - это всё, что необходимо сделать. + +### Ошибка! + +Если вдруг Вы стали получать ошибки в виде "Internal Server Error" или "No input file specified", попытайтесь изменить `.htaccess` следующее: + + RewriteRule ^(?:application|modules|system)\b - [F,L] + +Вместо параметра `\b` попробуйте использовать слеш: + + RewriteRule ^(application|modules|system)/ - [F,L] + +Если это не поможет, попробуйте изменить следующее: + + RewriteRule .* index.php/$0 [PT] + +На что-то более простое: + + RewriteRule .* index.php [PT] + +### Всё равно ошибка! + +Если всё ещё получаете ошибки, убедитесь, что Ваш хостинг предоставляет поддержку Apache `mod_rewrite`. Если у Вас есть доступ до изменения настроек Apache, то добавьте следующие строки в конфигурационный файл (зачастую это `httpd.conf`): + + + Order allow,deny + Allow from all + AllowOverride All + + +## NGINX + +Тяжело дать пример конфигурации nginx сервера, но можно использовать следующий пример для server блока: + + location / { + index index.php index.html index.htm; + try_files $uri $uri/ index.php$uri?$args; + } + + location ~ ^(.+\.php)(.*)$ { + fastcgi_split_path_info ^(.+\.php)(.*)$; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + + include fastcgi.conf; + + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + } + +Заметьте, что в данном примере используются [try_files](http://wiki.nginx.org/NginxHttpCoreModule#try_files) и [fastcgi_split_path_info](http://wiki.nginx.org/NginxHttpFcgiModule#fastcgi_split_path_info) свойства. + +[!!] Этот пример подразумевает, что Вы запускаете PHP как FastCGI сервер на порту 9000 и используете nginx v0.7.31 и выше. + +Если с этой конфигурацией Вы получаете ошибки, установите для nginx уровень логов в debug и проверьте access и error логи на предмет ошибок. \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/ru-ru/tutorials.urls.md b/includes/kohana/modules/userguide/guide/ru-ru/tutorials.urls.md new file mode 100644 index 00000000..d90e3371 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/tutorials.urls.md @@ -0,0 +1,160 @@ +# Маршруты, URL, и Ссылки + +В данном разделе будет раскрыты фундаментальные основы маршрутизации запросов Kohana, формирования URL и ссылок. + +## маршрутизация + +Как уже говорилось в секции [порядок выполнения](about.flow), запрос обрабатывается классом [Request], который ищет подходящий маршрут ([Route]) и загружает соответствующий контроллер для выполнения. Таким образом, система обеспечивает большую гибкость, а также интуитивно понятное поведение по умолчанию. + +Если Вы заглянете в `APPPATH/bootstrap.php`, то увидите следующий участок кода, который будет выполнен непосредственно до передачи запроса в вызов [Request::instance]: + + Route::set('default', '((/(/)))') + ->defaults(array( + 'controller' => 'welcome', + 'action' => 'index', + )); + +Так устанавливается машрут по умолчанию (`default`) с адресом по шаблону `((/(/)))`. Символы, заключенные в угловые скобки `<>`, являются *ключами*, а круглыми скобками - *необязательные* части адреса. В данном случае весь адрес является опциональным, так что и пустой адрес будет отработан, а дефолтные контроллер и экшен приведут к тому, что для обработки запроса загрузится класс `Controller_Welcome` и выполнится метод `action_index`. + +Заметьте, что в маршрутах Kohana вне `()<>` допустимы любые символы, а `/` не имеет специального значения. В маршруте по умолчанию `/` используется как разделитель, но пока регулярное выражение имеет смысл, нет никаких ограничений на форматирование Ваших маршрутов. + +### Директории + +Вы можете захотеть разместить некоторые из контроллеров в поддиректориях. Типовое решение для админской части сайта: + + Route::set('admin', 'admin(/(/(/)))') + ->defaults(array( + 'directory' => 'admin', + 'controller' => 'home', + 'action' => 'index', + )); + +Этот маршрут определяет, что адрес должен начинаться с `admin`, в результате значение директории (`admin`) будет взято из дефолтовых значений. Запрос к `admin/users/create` загрузит класс `Controller_Admin_Users` и вызовет метод `action_create`. + +### Шаблоны + +Система маршрутизации Kohana при поиске совпадений использует perl-совместимые регулярные выражения. По умолчанию ключи (окруженные `<>`) определяются шаблоном `[a-zA-Z0-9_]++`, но Вы можете указать свой шаблон для каждого используемого ключа. Для этого заполняется массив ключей и шаблонов, передающийся как дополнительный аргумент метода [Route::set]. Чтобы усложнить предыдущий пример, давайте представим, что у Вас есть админский раздел и секция подразделений (affiliates). Вы можете разделить их по разным маршрутам, а можете объявить как-то так: + + Route::set('sections', '(/(/(/)))', + array( + 'directory' => '(admin|affiliate)' + )) + ->defaults(array( + 'controller' => 'home', + 'action' => 'index', + )); + +Таким образом мы выделили два раздела сайта, 'admin' и 'affiliate', которые позволят разместить контроллеры по поддиректориям, в остальных случаях будет работать маршрут по умолчанию. + +### Еще примеры маршрутов + +Существует бесчисленное количество возможностей маршрутизации. Вот еще некоторые из них: + + /* + * Короткие адреса для авторизации + */ + Route::set('auth', '', + array( + 'action' => '(login|logout)' + )) + ->defaults(array( + 'controller' => 'auth' + )); + + /* + * Разноформатные ленты новостей + * 452346/comments.rss + * 5373.json + */ + Route::set('feeds', '(/).', + array( + 'user_id' => '\d+', + 'format' => '(rss|atom|json)', + )) + ->defaults(array( + 'controller' => 'feeds', + 'action' => 'status', + )); + + /* + * Статичные страницы + */ + Route::set('static', '.html', + array( + 'path' => '[a-zA-Z0-9_/]+', + )) + ->defaults(array( + 'controller' => 'static', + 'action' => 'index', + )); + + /* + * Не нравятся слэши? + * EditGallery:bahamas + * Watch:wakeboarding + */ + Route::set('gallery', '():', + array( + 'controller' => '[A-Z][a-z]++', + 'action' => '[A-Z][a-z]++', + )) + ->defaults(array( + 'controller' => 'Slideshow', + )); + + /* + * Быстрый поиск + */ + Route::set('search', ':', array('query' => '.*')) + ->defaults(array( + 'controller' => 'search', + 'action' => 'index', + )); + +Маршруты анализируются в том же порядке, в котором были добавлены, так что имейте в виду, что если Вы установили маршрут после загрузки модулей, в одном из загруженных модулей может быть определен маршрут, конфликтующий с Вашим. Поэтому дефолтный маршрут устанавливается последним, так что все пользовательские маршруты будут проверены до него. + +### Параметры запроса + +Директория, контроллер и экшен могут быть доступны через экземпляр класса [Request] одним из двух способов: + + $this->request->action; + Request::instance()->action; + +Все прочие ключи маршрута могут быть получены из контроллера так: + + $this->request->param('key_name'); + +Метод [Request::param] принимает дополнительный параметр, определяющий возвращаемое по умолчанию значение, если оно не было передано в адресе. Если метод вызван без параметром, все ключи будут возвращены в ассоциативном массиве. + +### Соглашения + +Установлено соглашение размещать Ваши маршруты в файле `MODPATH//init.php` модуля, если маршрут относится к данному модулю. Или просто вставить его в файл `APPPATH/bootstrap.php` до дефолтного маршрута, если он относится к приложению в целом. Конечно, они могут быть подгружены из внешнего файла или сгенерированы динамически. + +## Адреса URL + +Помимо мощных возможностей Kohana, есть методы для генерирования URL для Ваших маршрутов. Вы всегда можете определить адрес как строку с помощью [URL::site] для создания URL, примерно так: + + URL::site('admin/edit/user/'.$user_id); + +Однако Kohana дает возможность генерировать URL с учетом определенного маршрута. Это очень полезно, если Ваша маршрутизация может поменяться, поскольку это избавит от необходимости заново редактировать весь код, меняя прописанный там адрес. Вот пример динамической генерации, который использует приведенный ранее маршрут `feeds`: + + Route::get('feeds')->uri(array( + 'user_id' => $user_id, + 'action' => 'comments', + 'format' => 'rss' + )); + +Предположим, Вы решите позже сделать определение маршрута более говорящим, заменив его на `feeds/(/).`. Если Вы будете писать код с генерацией адресов как в примере выше, то не придется менять ни одной строчки кода! Если в адресе есть заключенный в скобки (опциональный) ключ, для которого не передано значение при генерации, и не приведено значение по умолчанию, то данная часть будет исключена из адреса. Примером будет ключ `(/)` дефолтного маршрута; он не будет присутствовать в сгенерированном URL если id не указан. + +Еще Вы можете часто использовать метод [Request::uri], который по сути делает то же самое, что вышеуказанный метод, только он подставляет текущие значения маршрута, директории, контроллера и экшена. Если текщий маршрут дефолтный, и адрес был `users/list`, для формирования адреса вида `users/view/$id` можно сделать так: + + $this->request->uri(array('action' => 'view', 'id' => $user_id)); + +Из шаблона предпочтительнее вызывать: + + Request::instance()->uri(array('action' => 'view', 'id' => $user_id)); + +## Ссылки + +[!!] Заглушка + diff --git a/includes/kohana/modules/userguide/guide/ru-ru/using.autoloading.md b/includes/kohana/modules/userguide/guide/ru-ru/using.autoloading.md new file mode 100644 index 00000000..778658ef --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/using.autoloading.md @@ -0,0 +1,96 @@ +# Загрузка классов + +Kohana использует все преимущества [автозагрузки](http://php.net/manual/language.oop5.autoload.php) в PHP. +Это позволяет не использовать вызовы [include](http://php.net/include) или [require](http://php.net/require) перед использованием класса. К примеру, когда Вы хотите использовать метод [Cookie::set], Вы всего лишь вызываете: + + Cookie::set('mycookie', 'any string value'); + +А для получения объекта [Encrypt] просто используйте [Encrypt::instance]: + + $encrypt = Encrypt::instance(); + +Классы загружаются с помощью метода [Kohana::auto_load], который осуществляет простое преобразование из имени класса в имя файла: + +1. Классы располагаются в директории `classes/` [файловой системы](about.filesystem) +2. Все знаки подчеркивания заменяются слэшами +2. Имена файлов должны быть в нижнем регистре + +Когда вызывается класс, который еще не был загружен (например `Session_Cookie`), Kohana будет искать файл под именем `classes/session/cookie.php` в файловой системе, с помощью [Kohana::find_file]. + +## Собственные загрузчики + +Системный загрузчик добавляется в файле `application/bootstrap.php` через вызов [spl_autoload_register](http://php.net/spl_autoload_register): + + spl_autoload_register(array('Kohana', 'auto_load')); + +Теперь [Kohana::auto_load] будет пытаться загрузить любой несуществующий класс при его первом использовании. + +# Прозрачное расширение классов {#class-extension} + +[Каскадная файловая система](about.filesystem) поддерживает прозрачное расширение классов. Например, класс [Cookie] определен в `SYSPATH/classes/cookie.php` так: + + class Cookie extends Kohana_Cookie {} + +Системные классы Kohana, как и многие модули, используют такое определение, так что практически все классы могут быть расширены. Это делается прозрачно для системы, создайте свой класс в `APPPATH/classes/cookie.php` для добавления собственных методов. + +[!!] **Никогда** не изменяйте файлы дистрибутива Kohana. Всегда вносите изменения в классы, используя расширения, так Вы избавитесь от головной боли при обновлении. + +К примеру, Вы хотите создать метод, который устанавливает зашифрованную куку с помощью класса [Encrypt]: + + encode((string) $value); + + parent::set($name, $value, $expiration); + } + + /** + * Gets an encrypted cookie. + * + * @uses Cookie::get + * @uses Encrypt::decode + */ + public static function decrypt($name, $default = NULL) + { + if ($value = parent::get($name, NULL)) + { + $value = Encrypt::instance(Cookie::$encryption)->decode($value); + } + + return isset($value) ? $value : $default; + } + + } // End Cookie + +Теперь вызов `Cookie::encrypt('secret', $data)` будет создавать шифрованную куку, которую можно расшифровать так: `$data = Cookie::decrypt('secret')`. + +## Многоуровневое расширение {#multiple-extensions} + +Если Вы расширяете классы Kohana в модуле, следует поддерживать прозрачное расширение. Вместо того, чтобы наследовать расширение [Cookie] от [Kohana_Cookie], создайте `MODPATH/mymod/encrypted/cookie.php`: + + class Encrypted_Cookie extends Kohana_Cookie { + + // Используйте методы encrypt() and decrypt(), описанные выше + + } + +Теперь создайте `MODPATH/mymod/cookie.php`: + + class Cookie extends Encrypted_Cookie {} + +Таким образом, пользователи смогут добавлять свои расширения в класс [Cookie], не затрагивая Ваши изменения. Однако, при следующем расширении класса [Cookie] придется наследоваться от `Encrypted_Cookie`, а не от `Kohana_Cookie`. \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/ru-ru/using.configuration.md b/includes/kohana/modules/userguide/guide/ru-ru/using.configuration.md new file mode 100644 index 00000000..1b8f7d96 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/using.configuration.md @@ -0,0 +1,58 @@ +# Основы + +Kohana использует для конфигурации как статические свойства, так и файлы. Статические свойства обычно применяются для статических классов, таких как [Cookie], [Security], и [Upload]. Файлы используются для объектов типа [Database], [Encrypt], и [Session]. + +Статические свойства могут быть установлены в `APPPATH/bootstrap.php` или с помощью [расширения классов](using.autoloading#class-extension). Преимущество статических свойств в том, что не требуется загрузки дополнительных файлов. Недостаток в том, что инициируется загрузка класса, когда свойство устанавливается (если Вы не используете для этого расширения). С другой стороны, использование расширений переопределит возможные расширения из других модулей. Так что рекомендуется выносить установку статических свойств в bootstrap. + +[!!] При использовании opcode-кэширования, например [APC](http://php.net/apc) или [eAccelerator](http://eaccelerator.net/), время загрузки класса значительно уменьшается. Настойчиво рекомендуем использовать opcode-кэширование на любом работающем сайте, независимо от размера. + +## Параметры инициализации + +Для каждой новой установки Kohana потребуется изменить настройки [Kohana::init] в `APPPATH/bootstrap.php`. Все опущенные настройки будут использовать дефолтные значения. Настройки могут быть получены и изменены позже, с помощью статических свойств класса [Kohana]. К примеру, для получения текущей кодировки, обратитесь к свойству [Kohana::$charset]. + +## Настройки безопасности + +Существует несколько параметров, которые необходимо поменять, чтобы сделать Kohana более защищенной. Самый важный из них - [Cookie::$salt], который используется для "подписывания" куков, что защищает их от подмены вне Kohana. + +Если Вы планируете использовать класс [Encrypt], Вам также понадобится создать файл конфигурации `encrypt` и установить значение `ключа` шифрования. Шифровальный ключ должен включать буквы, цифры и знаки для повышения безопасности. + + +[!!] **Не используйте хэш для ключа шифрования!** Поступая таким образом, Вы облегчаете его взлом. + +# Файлы конфигурации {#config-files} + +Файлы конфигурации несколько отличаются от прочих файлов [каскадной файловой системы](about.filesystem) тем, что они **сливаются** вместо того, чтобы переопределяться. Это означает, что все конфигурационные файлы с одинаковым путем будут объединены для получения итоговой конфигурации. Как результат, Вы можете перезаписать *отдельные* параметры вместо того, чтобы дублровать все содержимое файла. + +Файлы конфигурации являются обычными PHP-файлами, хранимыми в директории `config/`, они должны возвращать ассоциативный массив: + + 'value', + 'options' => array( + 'foo' => 'bar', + ), + ); + +В данном примере файл называется `myconf.php`, и Вы можете получить доступ к нему так: + + $config = Kohana::config('myconf'); + $options = $config['options']; + +[Kohana::config] также предоставляет быстрый доступ к отдельным ключам массива конфигурации, используя "точечные пути". + +Получим доступ к массиву "options": + + $options = Kohana::config('myconf.options'); + +А так извлечем ключ "foo" из массива "options": + + $foo = Kohana::config('myconf.options.foo'); + +Также массивы могут выступать в качестве объектов, если Вам так больше нравится: + + $options = Kohana::config('myconf')->options; + +Пожалуйста, обратите внимание, что Вы можете получить доступ только к корневым ключам через свойства объекта, все дочерние ключи доступны через стандартный синтаксис работы с массивами: + + $foo = Kohana::config('myconf')->options['foo']; diff --git a/includes/kohana/modules/userguide/guide/ru-ru/using.messages.md b/includes/kohana/modules/userguide/guide/ru-ru/using.messages.md new file mode 100644 index 00000000..baf08e41 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/using.messages.md @@ -0,0 +1,26 @@ +# Основы + +Сообщения Kohana - это понятные человеку строки, представленные в виде короткого слова или фразы, называемой "ключ". Сообщения извлекают с помощью метода [Kohana::message], который возвращает либо группу сообщений целиком, либо отдельное сообщение. + +Например, когда пользователь не вошел в систему и пытается зайти на страницу, требующую аутентифицикации, должна быть отображена ошибка типа "Вы должны залогиниться для доступа к данной странице". Это сообщение может храниться в файле `auth` под ключом `must_login`: + + $message = Kohana::message('auth', 'must_login'); + +Сообщения не переводятся автоматически. Для осуществления перевода используйте [функции перевода](using.translation): + + $translated = __(Kohana::message('auth', 'must_login')); + +[!!] В Kohana v2 система сообщений использовалась для перевода. Однако, рекомендуется использовать новую систему перевода вместо системы сообщений, так как она выведет понятный текст даже если перевод не найден. + +## Файлы сообщений + +Все файлы сообщений являются PHP-файлами, хранимыми в директории `messages/`, и возвращают ассоциативные массивы: + + 'Вы должны залогиниться для доступа к этой странице', + 'no_access' => 'У Вас недостаточно прав для доступа к странице', + ); + +Файлы сообщений похожи на [файлы настроек](using.configuration#config-files) - они тоже собираются вместе. Это означает, что все сообщения, хранимые в файлах с именем `auth`, будут объединены в один массив, так что нет необходимости дублировать все сообщения, если Вы создаете новый файл `auth`. diff --git a/includes/kohana/modules/userguide/guide/ru-ru/using.sessions.md b/includes/kohana/modules/userguide/guide/ru-ru/using.sessions.md new file mode 100644 index 00000000..bddfd5fe --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/using.sessions.md @@ -0,0 +1,223 @@ +# Сессии и Куки + +Kohana предоставляет пару классов, которые облегчают работу с куками и сессиями. На верхнем уровне и сессии, и куки обеспечивают одни и те же функции. Они позволяют разработчику хранить временную или постоянную инфомацию о конкретном клиенте для дальнейшего использования. + +Куки следует использовать для хранения публичных (некритичных) данных, неизменных в течении длительного времени. Например, хранить идентификатор пользователя или предпочитаемый язык. Используйте класс [Cookie] для установки и получения кук. + +[!!] Kohana работает с "подписанными" куками. Каждая хранимая кука содержит хэша для предотвращения подмены значения куки. Этот хэш генерируется методом [Cookie::salt], который учитывает свойство [Cookie::$salt]. Вам следует [изменить настройки](using.configuration), когда будете опубликовывать свое приложение. + +Сессии лучше использовать для хранения временных или секретных данных. Крайне критичную информацию стоит хранить в классе [Session] с драйвером "database" или "native". Когда используется драйвер "cookie", сессия должна быть зашифрована. + +[!!] Больше информации о работе с переменными сессии Вы можете получить в статье [семь смертных грехов сессий](http://lists.nyphp.org/pipermail/talk/2006-December/020358.html). + +# Хранение, извлечение и удаление данных + +[Cookie] и [Session] предоставляют очень схожий API для хранения данных. Главное отличие между ними в том, что доступ к сессии осуществляется как к объекту, а к кукам - как статическому классу (хэлпер). + +Получить объект сессии можно посредством метода [Session::instance]: + + // Get the session instance + $session = Session::instance(); + +Вы можете также получить все данные сессии с помощью метода [Session::as_array]: + + // Get all of the session data as an array + $data = $session->as_array(); + +Также есть возможность переписать глобальную переменную `$_SESSION`. чтобы работать с сессиями в более привычном, стандартном для PHP стиле: + + // Overload $_SESSION with the session data + $_SESSION =& $session->as_array(); + + // Set session data + $_SESSION[$key] = $value; + +## Хранение данных {#setting} + +Для сохранения данных сессии или куки применяется метод `set`: + + // Set session data + $session->set($key, $value); + + // Set cookie data + Cookie::set($key, $value); + + // Store a user id + $session->set('user_id', 10); + Cookie::set('user_id', 10); + +## Получение данных {#getting} + +Извлечение данных сессии или кук возможно посредством метода `get`: + + // Get session data + $data = $session->get($key, $default_value); + + // Get cookie data + $data = Cookie::get($key, $default_value); + + // Get the user id + $user = $session->get('user_id'); + $user = Cookie::get('user_id'); + +## Удаление данных {#deleting} + +Метод `delete` позволяет удалить данные из сессии или кук: + + // Delete session data + $session->delete($key); + + // Delete cookie data + Cookie::delete($key); + + // Delete the user id + $session->delete('user_id'); + Cookie::delete('user_id'); + +# Настройка {#configuration} + +И куки, и сессии имеют несколько параметров, которые влияют на механизм хранение данных. Всегда проверяйте их перед завершением приложения, так как многие из них будут напрямую влиять на безопасность Вашего приложения. + +## Настройка кук + +Все настройки изменяются через статические свойства. Вы можете изменить их либо через `bootstrap.php`, либо посредством [расширения классов](using.autoloading#class-extension). + +Наиболее важный параметр это [Cookie::$salt], он используется для шифрования подписи. Значение необходимо поменять и держать в тайне: + + Cookie::$salt = 'your secret is safe with me'; + +[!!] Изменение данного значения сделает недействительными все сохраненные ранее куки. + +По умолчанию куки хранятся до закрытия браузера. Чтобы указать свое значение для времени жизни, измените параметр [Cookie::$expiration]: + + // Set cookies to expire after 1 week + Cookie::$expiration = 604800; + + // Alternative to using raw integers, for better clarity + Cookie::$expiration = Date::WEEK; + +Адрес, с которого куки могут быть доступны, может быть ограничен параметром [Cookie::$path]. + + // Allow cookies only when going to /public/* + Cookie::$path = '/public/'; + +Домен, на котором куки будут доступны, указан в свойстве [Cookie::$domain]. + + // Allow cookies only on the domain www.example.com + Cookie::$domain = 'www.example.com'; + +Если Вы хотите сделать куку доступной для всех поддоменов, поставьте точку перед началом домена + + // Allow cookies to be accessed on example.com and *.example.com + Cookie::$domain = '.example.com'; + +Чтобы разрешить куки только по защищенному (HTTPS) соединению, установите [Cookie::$secure] параметр. + + // Allow cookies to be accessed only on a secure connection + Cookie::$secure = TRUE; + + // Allow cookies to be accessed on any connection + Cookie::$secure = FALSE; + +Защитите куки от доступа через Javascript, изменив параметр [Cookie::$httponly]. + + // Make cookies inaccessible to Javascript + Cookie::$httponly = TRUE; + +## Драйверы сессии {#adapters} + +При создании или доступе к объекту класс [Session] Вы можете выбрать, какой драйвер использовать. Доступны следующие драйверы: + +Native +: Хранит данные в стандартном месте на диске web-сервера. Путь указывается в параметре [session.save_path](http://php.net/manual/session.configuration.php#ini.session.save-path) файла `php.ini` или переопределяется методом [ini_set](http://php.net/ini_set). + +Database +: Хранит информацию в базе данных с помощью класса [Session_Database]. Для работы требуется подключенный модуль [Database]. + +Cookie +: Хранит данные в куках, с помощью класса [Cookie]. **Для данного драйвера предельный размер сессии будет равен 4Кб ** + +Драйвер по умолчанию может быть установлен в [Session::$default]. Изначально это драйвер "native". + +[!!] Как и с куками, установка параметра "lifetime" в "0" означает, что сессия будет уничтожена после закрытия браузера. + +### Настройка драйвера сессии + +Вы можете применить настройки для каждого драйвера, создав конфигурационный файл `APPPATH/config/session.php`. Следующий пример настроек определяет конфигурацию для каждого драйвера: + + return array( + 'native' => array( + 'name' => 'session_name', + 'lifetime' => 43200, + ), + 'cookie' => array( + 'name' => 'cookie_name', + 'encrypted' => TRUE, + 'lifetime' => 43200, + ), + 'database' => array( + 'name' => 'cookie_name', + 'encrypted' => TRUE, + 'lifetime' => 43200, + 'group' => 'default', + 'table' => 'table_name', + 'columns' => array( + 'session_id' => 'session_id', + 'last_active' => 'last_active', + 'contents' => 'contents' + ), + 'gc' => 500, + ), + ); + +#### Драйвер Native {#adapter-native} + +Тип | Параметр | Описание | По умолчанию +----------|-----------|---------------------------------------------------|----------- +`string` | name | имя сессии | `"session"` +`integer` | lifetime | время жизни сессии (в секундах) | `0` + +#### Cookie Adapter {#adapter-cookie} + +Тип | Параметр | Описание | По умолчанию +----------|-----------|---------------------------------------------------|----------- +`string` | name | имя куки, используемой для хранения сессии | `"session"` +`boolean` | encrypted | шифровать данные с помощью [Encrypt]? | `FALSE` +`integer` | lifetime | время жизни сессии (в секундах) | `0` + +#### Database Adapter {#adapter-database} + +Тип | Параметр | Описание | По умолчанию +----------|-----------|---------------------------------------------------|----------- +`string` | group | название группы [Database::instance] | `"default"` +`string` | table | имя таблицы, в которой хранить данные | `"sessions"` +`array` | columns | ассоциативный массив псевдонимов полей | `array` +`integer` | gc | дает 1:x шанс, что запустится сборка мусора | `500` +`string` | name | имя куки, используемой для хранения сессии | `"session"` +`boolean` | encrypted | шифровать данные с помощью [Encrypt]? | `FALSE` +`integer` | lifetime | время жизни сессии (в секундах) | `0` + +##### Структура таблицы + +Вам придется создать таблицу для хранения сессии в базе данных. Вот структура по умолчанию: + + CREATE TABLE `sessions` ( + `session_id` VARCHAR(24) NOT NULL, + `last_active` INT UNSIGNED NOT NULL, + `contents` TEXT NOT NULL, + PRIMARY KEY (`session_id`), + INDEX (`last_active`) + ) ENGINE = MYISAM; + +##### Поля таблицы + +Вы можете изменить имя полей, чтобы использовать существующую таблицу. По умолчанию используется имя ключа. + +session_id +: название поля "id" + +last_active +: метка времени UNIX для последнего времени обновления сессии + +contents +: данные сессии, хранимые в виде сериализованной и (необязательно) зашифрованной строки diff --git a/includes/kohana/modules/userguide/guide/ru-ru/using.translation.md b/includes/kohana/modules/userguide/guide/ru-ru/using.translation.md new file mode 100644 index 00000000..a98ecbbf --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/using.translation.md @@ -0,0 +1 @@ +# Интернационализация, перевод \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/ru-ru/using.views.md b/includes/kohana/modules/userguide/guide/ru-ru/using.views.md new file mode 100644 index 00000000..a519bd3d --- /dev/null +++ b/includes/kohana/modules/userguide/guide/ru-ru/using.views.md @@ -0,0 +1,118 @@ +# Использование представлений + +Представления - файлы, содержащие отображаемую информацию Вашего приложения. Чаще всего это HTML, CSS и Javascript, но может быть чем угодно, например XML или JSON для AJAX-вызовов. Цель представлений - хранить эту информацию отдельно от логики приложения для облегчения повторного использования и более чистого кода. + +Несмотря на это, представления сами по себе могут содержать код, используемый для отображения сохраненных в них данных. Например, циклический перебор элементов массива данных о продукте и отображение каждого в отдельном табличном ряду. Представления есть PHP-файлы, так что Вы можете свободно использовать там любой код, как обычно. + +# Создание файлов представлений + +Файлы представлений располагаются в директории `views` [файловой системы](about.filesystem). Вы также можете создавать поддиректории в ней для упорядочивания файлов. Все приведенные ниже примеры файлов являются допустимыми: + + APPPATH/views/home.php + APPPATH/views/pages/about.php + APPPATH/views/products/details.php + MODPATH/error/views/errors/404.php + MODPATH/common/views/template.php + +## Загрузка представлений + +Объект [View] обычно создается в контроллере ([Controller]) с помощью метода [View::factory]. Чаще всего представление записывается в свойство [Request::$response] или в другое представление. + + public function action_about() + { + $this->request->response = View::factory('pages/about'); + } + +Когда представление сохранено в [Request::$response], как в примере выше, оно будет автоматически отображено при необходимости. Для получения сгенерированного вывода представления используйте метод [View::render] или просто преобразуйте в строку. Когда представление генерируется, файл представления загружается, и формируется HTML. + + public function action_index() + { + $view = View::factory('pages/about'); + + // Render the view + $about_page = $view->render(); + + // Or just type cast it to a string + $about_page = (string) $view; + + $this->request->response = $about_page; + } + +## Переменные в представлениях + +Как только представление было загружено, к нему могут быть присоединены переменные, методами [View::set] и [View::bind]. + + public function action_roadtrip() + { + $view = View::factory('user/roadtrip') + ->set('places', array('Rome', 'Paris', 'London', 'New York', 'Tokyo')); + ->bind('user', $this->user); + + // The view will have $places and $user variables + $this->request->response = $view; + } + +[!!] Единственная разница между `set()` и `bind()` в том, что `bind()` присоединяет переменную по ссылке. Если Вы вызываете `bind()` переменной до того, как она определена, переменная будет создана со значением `NULL`. + +### Глобальные переменные + +Приложение может иметь несколько представлений, которым нужен доступ к одним и тем же переменным. Например, чтобы отобразить заголовок страницы и в шапке представления, и в теле содержимого страницы. Вы можете создать переменные, которые будут доступны в любом представлении, используя [View::set_global] и [View::bind_global]. + + // Assign $page_title to all views + View::bind_global('page_title', $page_title); + +Пусть приложение имеет три представления, которые генерируют главную страницу: `template`, `template/sidebar`, и `pages/home`. Сперва, напишем абстрактный контроллер для создания шаблона: + + abstract class Controller_Website extends Controller_Template { + + public $page_title; + + public function before() + { + parent::before(); + + // Make $page_title available to all views + View::bind_global('page_title', $this->page_title); + + // Load $sidebar into the template as a view + $this->template->sidebar = View::factory('template/sidebar'); + } + + } + +Далее, главный контроллер будет расширять `Controller_Website`: + + class Controller_Home extends Controller_Website { + + public function action_index() + { + $this->page_title = 'Home'; + + $this->template->content = View::factory('pages/home'); + } + + } + +## Представления внутри представлений + +Если Вы хотите вложить одно представление в другое, есть два варианта. Используя [View::factory], Вы можете его заключить в текущем представлении. Это означает, что Вы должны будут передать в него все необходимые переменные методами [View::set] и [View::bind]: + + // Only the $user variable will be available in "views/user/login.php" + bind('user', $user) ?> + +Другой способ - подключение представлений напрямую, что делает все текущие переменные доступными в подключаемом представлении: + + // Any variable defined in this view will be included in "views/message.php" + + +Естественно, Вы также можете загрузить объект [Request] в представление: + + execute() ?> + +Это пример [HMVC](about.mvc), который делает возможным создавать и считывать вызовы других URL Вашего приложения. + +# Переход с версии 2.x + +В отличие от версии Kohana 2.x, представления не создаются в контексте текущего контроллера, так что Вы не сможете использовать `$this` в качестве контроллера, в который загружено данное представление. Контроллер должен быть передан туда явно: + + $view->bind('controller', $this); diff --git a/includes/kohana/modules/userguide/guide/security.cookies.md b/includes/kohana/modules/userguide/guide/security.cookies.md new file mode 100644 index 00000000..920e9c18 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/security.cookies.md @@ -0,0 +1,3 @@ +# Cookie Security + +[!!] stub diff --git a/includes/kohana/modules/userguide/guide/security.database.md b/includes/kohana/modules/userguide/guide/security.database.md new file mode 100644 index 00000000..f1d09bb5 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/security.database.md @@ -0,0 +1,3 @@ +# Database Security + +[!!] stub diff --git a/includes/kohana/modules/userguide/guide/security.validation.md b/includes/kohana/modules/userguide/guide/security.validation.md new file mode 100644 index 00000000..6a4e55ec --- /dev/null +++ b/includes/kohana/modules/userguide/guide/security.validation.md @@ -0,0 +1,245 @@ +# Validation + +Validation can be performed on any array using the [Validate] class. Labels, filters, rules, and callbacks can be attached to a Validate object by the array key, called a "field name". + +labels +: A label is a human-readable version of the field name. + +filters +: A filter modifies the value of an field before rules and callbacks are run. + +rules +: A rule is a check on a field that returns `TRUE` or `FALSE`. If a rule + returns `FALSE`, an error will be added to the field. + +callbacks +: A callback is custom method that can access the entire Validate object. + The return value of a callback is ignored. Instead, the callback must + manually add an error to the object using [Validate::error] on failure. + +[!!] Note that [Validate] callbacks and [PHP callbacks](http://php.net/manual/language.pseudo-types.php#language.types.callback) are not the same. + +Using `TRUE` as the field name when adding a filter, rule, or callback will by applied to all named fields. + +**The [Validate] object will remove all fields from the array that have not been specifically named by a label, filter, rule, or callback. This prevents access to fields that have not been validated as a security precaution.** + +Creating a validation object is done using the [Validate::factory] method: + + $post = Validate::factory($_POST); + +[!!] The `$post` object will be used for the rest of this tutorial. This tutorial will show you how to validate the registration of a new user. + +### Default Rules + +Validation also comes with several default rules: + +Rule name | Function +------------------------- |------------------------------------------------- +[Validate::not_empty] | Value must be a non-empty value +[Validate::regex] | Match the value against a regular expression +[Validate::min_length] | Minimum number of characters for value +[Validate::max_length] | Maximum number of characters for value +[Validate::exact_length] | Value must be an exact number of characters +[Validate::email] | An email address is required +[Validate::email_domain] | Check that the domain of the email exists +[Validate::url] | Value must be a URL +[Validate::ip] | Value must be an IP address +[Validate::phone] | Value must be a phone number +[Validate::credit_card] | Require a credit card number +[Validate::date] | Value must be a date (and time) +[Validate::alpha] | Only alpha characters allowed +[Validate::alpha_dash] | Only alpha and hyphens allowed +[Validate::alpha_numeric] | Only alpha and numbers allowed +[Validate::digit] | Value must be an integer digit +[Validate::decimal] | Value must be a decimal or float value +[Validate::numeric] | Only numeric characters allowed +[Validate::range] | Value must be within a range +[Validate::color] | Value must be a valid HEX color +[Validate::matches] | Value matches another field value + +[!!] Any method that exists within the [Validate] class may be used as a validation rule without specifying a complete callback. For example, adding `'not_empty'` is the same as `array('Validate', 'not_empty')`. + +## Adding Filters + +All validation filters are defined as a field name, a method or function (using the [PHP callback](http://php.net/manual/language.pseudo-types.php#language.types.callback) syntax), and an array of parameters: + + $object->filter($field, $callback, $parameter); + +Filters modify the field value before it is checked using rules or callbacks. + +If we wanted to convert the "username" field to lowercase: + + $post->filter('username', 'strtolower'); + +If we wanted to remove all leading and trailing whitespace from *all* fields: + + $post->filter(TRUE, 'trim'); + +## Adding Rules + +All validation rules are defined as a field name, a method or function (using the [PHP callback](http://php.net/callback) syntax), and an array of parameters: + + $object->rule($field, $callback, $parameter); + +To start our example, we will perform validation on a `$_POST` array that contains user registration information: + + $post = Validate::factory($_POST); + +Next we need to process the POST'ed information using [Validate]. To start, we need to add some rules: + + $post + ->rule('username', 'not_empty') + ->rule('username', 'regex', array('/^[a-z_.]++$/iD')) + + ->rule('password', 'not_empty') + ->rule('password', 'min_length', array('6')) + ->rule('confirm', 'matches', array('password')) + + ->rule('use_ssl', 'not_empty'); + +Any existing PHP function can also be used a rule. For instance, if we want to check if the user entered a proper value for the SSL question: + + $post->rule('use_ssl', 'in_array', array(array('yes', 'no'))); + +Note that all array parameters must still be wrapped in an array! Without the wrapping array, `in_array` would be called as `in_array($value, 'yes', 'no')`, which would result in a PHP error. + +Any custom rules can be added using a [PHP callback](http://php.net/manual/language.pseudo-types.php#language.types.callback]: + + $post->rule('username', 'User_Model::unique_username'); + +[!!] Currently (v3.0.7) it is not possible to use an object for a rule, only static methods and functions. + +The method `User_Model::unique_username()` would be defined similar to: + + public static function unique_username($username) + { + // Check if the username already exists in the database + return ! DB::select(array(DB::expr('COUNT(username)'), 'total')) + ->from('users') + ->where('username', '=', $username) + ->execute() + ->get('total'); + } + +[!!] Custom rules allow many additional checks to be reused for multiple purposes. These methods will almost always exist in a model, but may be defined in any class. + +## Adding callbacks + +All validation callbacks are defined as a field name and a method or function (using the [PHP callback](http://php.net/manual/language.pseudo-types.php#language.types.callback) syntax): + + $object->callback($field, $callback); + +The user password must be hashed if it validates, so we will hash it using a callback: + + $post->callback('password', array($model, 'hash_password')); + +This would assume that the `$model->hash_password()` method would be defined similar to: + + public function hash_password(Validate $array, $field) + { + if ($array[$field]) + { + // Hash the password if it exists + $array[$field] = sha1($array[$field]); + } + } + +# A Complete Example + +First, we need a [View] that contains the HTML form, which will be placed in `application/views/user/register.php`: + + + +

        Some errors were encountered, please check the details you entered.

        +
          + +
        • + + + +
          +
          +
          + +
          +
          +
          Passwords must be at least 6 characters long.
          +
          +
          + +
          +
          'Always', 'no' => 'Only when necessary'), $post['use_ssl']) ?>
          +
          For security, SSL is always used when making payments.
          +
          + + + + +[!!] This example uses the [Form] helper extensively. Using [Form] instead of writing HTML ensures that all of the form inputs will properly handle input that includes HTML characters. If you prefer to write the HTML yourself, be sure to use [HTML::chars] to escape user input. + +Next, we need a controller and action to process the registration, which will be placed in `application/classes/controller/user.php`: + + class Controller_User extends Controller { + + public function action_register() + { + $user = Model::factory('user'); + + $post = Validate::factory($_POST) + ->filter(TRUE, 'trim') + + ->filter('username', 'strtolower') + + ->rule('username', 'not_empty') + ->rule('username', 'regex', array('/^[a-z_.]++$/iD')) + ->rule('username', array($user, 'unique_username')) + + ->rule('password', 'not_empty') + ->rule('password', 'min_length', array('6')) + ->rule('confirm', 'matches', array('password')) + + ->rule('use_ssl', 'not_empty') + ->rule('use_ssl', 'in_array', array(array('yes', 'no'))) + + ->callback('password', array($user, 'hash_password')); + + if ($post->check()) + { + // Data has been validated, register the user + $user->register($post); + + // Always redirect after a successful POST to prevent refresh warnings + $this->request->redirect('user/profile'); + } + + // Validation failed, collect the errors + $errors = $post->errors('user'); + + // Display the registration form + $this->request->response = View::factory('user/register') + ->bind('post', $post) + ->bind('errors', $errors); + } + + } + +We will also need a user model, which will be placed in `application/classes/model/user.php`: + + class Model_User extends Model { + + public function register($array) + { + // Create a new user record in the database + $id = DB::insert(array_keys($array)) + ->values($array) + ->execute(); + + // Save the new user id to a cookie + cookie::set('user', $id); + + return $id; + } + + } + +That is it, we have a complete user registration example that properly checks user input! diff --git a/includes/kohana/modules/userguide/guide/security.xss.md b/includes/kohana/modules/userguide/guide/security.xss.md new file mode 100644 index 00000000..71edbfab --- /dev/null +++ b/includes/kohana/modules/userguide/guide/security.xss.md @@ -0,0 +1,15 @@ +# Cross-Site Scripting (XSS) Security + +The first step to preventing [XSS](http://wikipedia.org/wiki/Cross-Site_Scripting) attacks is knowing when you need to protect yourself. XSS can only be triggered when it is displayed within HTML content, sometimes via a form input or being displayed from database results. Any global variable that contains client information can be tainted. This includes `$_GET`, `$_POST`, and `$_COOKIE` data. + +## Prevention + +There are a few simple rules to follow to guard your application HTML against XSS. The first is to use the [Security::xss] method to clean any input data that comes from a global variable. If you do not want HTML in a variable, use [strip_tags](http://php.net/strip_tags) to remove all unwanted HTML tags from a value. + +[!!] If you allow users to submit HTML to your application, it is highly recommended to use an HTML cleaning tool such as [HTML Purifier](http://htmlpurifier.org/) or [HTML Tidy](http://php.net/tidy). + +The second is to always escape data when inserting into HTML. The [HTML] class provides generators for many common tags, including script and stylesheet links, anchors, images, and email (mailto) links. Any untrusted content should be escaped using [HTML::chars]. + +## References + +* [OWASP XSS Cheat Sheet](http://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet) diff --git a/includes/kohana/modules/userguide/guide/tutorials.databases.md b/includes/kohana/modules/userguide/guide/tutorials.databases.md new file mode 100644 index 00000000..c611d3b2 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/tutorials.databases.md @@ -0,0 +1,248 @@ +# Databases {#top} + +Kohana 3.0 comes with a robust module to working with databases. By default the database module supports drivers for [MySQL](http://php.net/mysql) and [PDO](http://php.net/pdo). + +The database module is included with the Kohana 3.0 install but needs to be enabled before you can use it. In your `application/bootstrap.php` file modify the call to [Kohana::modules] and include the database module: + + Kohana::modules(array( + ... + 'database' => MODPATH.'database', + ... + )); + +## Configuration {#configuration} + +After the module has been enabled you will need to provide a configuration file so that the module knows how to connect to your database. An example config file can be found at `modules/database/config/database.php`. + +The structure of a database configuration group, called an "instance", looks like this: + + string INSTANCE_NAME => array( + 'type' => string DATABASE_TYPE, + 'connection' => array CONNECTION_ARRAY, + 'table_prefix' => string TABLE_PREFIX, + 'charset' => string CHARACTER_SET, + 'profiling' => boolean QUERY_PROFILING, + ), + +[!!] Multiple instances of these settings can be defined within the configuration file. + +Understanding each of these settings is important. + +INSTANCE_NAME +: Connections can be named anything you want, but you should always have at least one connection called "default". + +DATABASE_TYPE +: One of the installed database drivers. Kohana comes with "mysql" and "pdo" drivers. + +CONNECTION_ARRAY +: Specific driver options for connecting to your database. (Driver options are explained [below](#connection_settings).) + +TABLE_PREFIX +: Prefix that will be added to all table names by the [query builder](#query_building). + +QUERY_PROFILING +: Enables [profiling](debugging.profiling) of database queries. + +### Example + +The example file below shows 2 MySQL connections, one local and one remote. + + return array + ( + 'default' => array + ( + 'type' => 'mysql', + 'connection' => array( + 'hostname' => 'localhost', + 'username' => 'dbuser', + 'password' => 'mypassword', + 'persistent' => FALSE, + 'database' => 'my_db_name', + ), + 'table_prefix' => '', + 'charset' => 'utf8', + 'profiling' => TRUE, + ), + 'remote' => array( + 'type' => 'mysql', + 'connection' => array( + 'hostname' => '55.55.55.55', + 'username' => 'remote_user', + 'password' => 'mypassword', + 'persistent' => FALSE, + 'database' => 'my_remote_db_name', + ), + 'table_prefix' => '', + 'charset' => 'utf8', + 'profiling' => TRUE, + ), + ); + +### Connection Settings {#connection_settings} + +Every database driver has different connection settings. + +#### MySQL + +A MySQL database can accept the following options in the `connection` array: + +Type | Option | Description | Default value +----------|------------|----------------------------| ------------------------- +`string` | hostname | Hostname of the database | `localhost` +`integer` | port | Port number | `NULL` +`string` | socket | UNIX socket | `NULL` +`string` | username | Database username | `NULL` +`string` | password | Database password | `NULL` +`boolean` | persistent | Persistent connections | `FALSE` +`string` | database | Database name | `kohana` + +#### PDO + +A PDO database can accept these options in the `connection` array: + +Type | Option | Description | Default value +----------|------------|----------------------------| ------------------------- +`string` | dsn | PDO data source identifier | `localhost` +`string` | username | Database username | `NULL` +`string` | password | Database password | `NULL` +`boolean` | persistent | Persistent connections | `FALSE` + +[!!] If you are using PDO and are not sure what to use for the `dsn` option, review [PDO::__construct](http://php.net/pdo.construct). + +## Connections and Instances {#connections} + +Each configuration group is referred to as a database instance. Each instance can be accessed by calling [Database::instance]: + + $default = Database::instance(); + $remote = Database::instance('remote'); + +To disconnect the database, simply destroy the object: + + unset($default, Database::$instances['default']); + +If you want to disconnect all of the database instances at once: + + Database::$instances = array(); + +## Making Queries {#making_queries} + +There are two different ways to make queries. The simplest way to make a query is to use the [Database_Query], via [DB::query], to create queries. These queries are called "prepared statements" and allow you to set query parameters which are automatically escaped. The second way to make a query is by building the query using method calls. This is done using the [query builder](#query_building). + +[!!] All queries are run using the `execute` method, which accepts a [Database] object or instance name. See [Database_Query::execute] for more information. + +### Prepared Statements + +Using prepared statements allows you to write SQL queries manually while still escaping the query values automatically to prevent [SQL injection](http://wikipedia.org/wiki/SQL_Injection). Creating a query is simple: + + $query = DB::query(Database::SELECT, 'SELECT * FROM users WHERE username = :user'); + +The [DB::query] factory method creates a new [Database_Query] class for us, to allow method chaining. The query contains a `:user` parameter, which we can assign to a value: + + $query->param(':user', 'john'); + +[!!] Parameter names can be any string, as they are replaced using [strtr](http://php.net/strtr). It is highly recommended to **not** use dollars signs as parameter names to prevent confusion. + +If you want to display the SQL that will be executed, simply cast the object to a string: + + echo Kohana::debug((string) $query); + // Should display: + // SELECT * FROM users WHERE username = 'john' + +You can also update the `:user` parameter by calling [Database_Query::param] again: + + $query->param(':user', $_GET['search']); + +[!!] If you want to set multiple parameters at once, you can use [Database_Query::parameters]. + +Once you have assigned something to each of the parameters, you can execute the query: + + $query->execute(); + +It is also possible to bind a parameter to a variable, using a [variable reference]((http://php.net/language.references.whatdo)). This can be extremely useful when running the same query many times: + + $query = DB::query(Database::INSERT, 'INSERT INTO users (username, password) VALUES (:user, :pass)') + ->bind(':user', $username) + ->bind(':pass', $password); + + foreach ($new_users as $username => $password) + { + $query->execute(); + } + +In the above example, the variables `$username` and `$password` are changed for every loop of the `foreach` statement. When the parameter changes, it effectively changes the `:user` and `:pass` query parameters. Careful parameter binding can save a lot of code when it is used properly. + +### Query Building {#query_building} + +Creating queries dynamically using objects and methods allows queries to be written very quickly in an agnostic way. Query building also adds identifier (table and column name) quoting, as well as value quoting. + +[!!] At this time, it is not possible to combine query building with prepared statements. + +#### SELECT + +Each type of database query is represented by a different class, each with their own methods. For instance, to create a SELECT query, we use [DB::select]: + + $query = DB::select()->from('users')->where('username', '=', 'john'); + +By default, [DB::select] will select all columns (`SELECT * ...`), but you can also specify which columns you want returned: + + $query = DB::select('username', 'password')->from('users')->where('username', '=', 'john'); + +Now take a minute to look at what this method chain is doing. First, we create a new selection object using the [DB::select] method. Next, we set table(s) using the `from` method. Last, we search for a specific records using the `where` method. We can display the SQL that will be executed by casting the query to a string: + + echo Kohana::debug((string) $query); + // Should display: + // SELECT `username`, `password` FROM `users` WHERE `username` = 'john' + +Notice how the column and table names are automatically escaped, as well as the values? This is one of the key benefits of using the query builder. + +It is also possible to create `AS` aliases when selecting: + + $query = DB::select(array('username', 'u'), array('password', 'p'))->from('users'); + +This query would generate the following SQL: + + SELECT `username` AS `u`, `password` AS `p` FROM `users` + +#### INSERT + +To create records into the database, use [DB::insert] to create an INSERT query: + + $query = DB::insert('users', array('username', 'password'))->values(array('fred', 'p@5sW0Rd')); + +This query would generate the following SQL: + + INSERT INTO `users` (`username`, `password`) VALUES ('fred', 'p@5sW0Rd') + +#### UPDATE + +To modify an existing record, use [DB::update] to create an UPDATE query: + + $query = DB::update('users')->set(array('username' => 'jane'))->where('username', '=', 'john'); + +This query would generate the following SQL: + + UPDATE `users` SET `username` = 'jane' WHERE `username` = 'john' + +#### DELETE + +To remove an existing record, use [DB::delete] to create a DELETE query: + + $query = DB::delete('users')->where('username', 'IN', array('john', 'jane')); + +This query would generate the following SQL: + + DELETE FROM `users` WHERE `username` IN ('john', 'jane') + +#### Database Functions {#database_functions} + +Eventually you will probably run into a situation where you need to call `COUNT` or some other database function within your query. The query builder supports these functions in two ways. The first is by using quotes within aliases: + + $query = DB::select(array('COUNT("username")', 'total_users'))->from('users'); + +This looks almost exactly the same as a standard `AS` alias, but note how the column name is wrapped in double quotes. Any time a double-quoted value appears inside of a column name, **only** the part inside the double quotes will be escaped. This query would generate the following SQL: + + SELECT COUNT(`username`) AS `total_users` FROM `users` + +#### Complex Expressions + +Quoted aliases will solve most problems, but from time to time you may run into a situation where you need a complex expression. In these cases, you will need to use a database expression created with [DB::expr]. A database expression is taken as direct input and no escaping is performed. diff --git a/includes/kohana/modules/userguide/guide/tutorials.git.md b/includes/kohana/modules/userguide/guide/tutorials.git.md new file mode 100644 index 00000000..1b44a072 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/tutorials.git.md @@ -0,0 +1,149 @@ +# Working With Git + +Kohana uses [git](http://git-scm.com/) for version control and [github](http://github.com/kohana) for collaboration. This tutorial will show you how to use git and github to build a simple application. + +## Installing and setting up Git on your machine + +### Installing Git + +- OSX: [Git-OSX](http://code.google.com/p/git-osx-installer/) +- Windows: [Msygit](http://code.google.com/p/msysgit/) +- Or download it from [git-site](http://git-scm.com/) and install it manually (see git website) + +### Basic global settings + + git config --global user.name "Your Name" + git config --global user.email "youremail@website.com" + +### Additional but preferable settings + +To have a better visualisation of the git commandos and repositories in your command-line, you can set these: + + git config --global color.diff auto + git config --global color.status auto + git config --global color.branch auto + +### Setting auto-completion + +[!!] These lines are only to use on an OSX machine + +These lines will do all the dirty work for you, so auto-completion can work for your git-environment + + cd /tmp + git clone git://git.kernel.org/pub/scm/git/git.git + cd git + git checkout v`git --version | awk '{print $3}'` + cp contrib/completion/git-completion.bash ~/.git-completion.bash + cd ~ + rm -rf /tmp/git + echo -e "source ~/.git-completion.bash" >> .profile + +### Always use LF line endings + +This is the convention that we make for Kohana. Please set this settings for your own good and especially if you want to contribute to the Kohana community. + + git config --global core.autocrlf input + git config --global core.savecrlf true + +[!!] More information about line endings at [github](http://help.github.com/dealing-with-lineendings/) + +### More information to get you on the track + +- [Git Screencasts](http://www.gitcasts.com/) +- [Git Reference](http://gitref.org/) +- [Pro Git book](http://progit.org/book/) + +## Initial Structure + +[!!] This tutorial will assume that your web server is already set up, and you are going to create a new application at . + +Using your console, change to the empty directory `gitorial` and run `git init`. This will create the bare structure for a new git repository. + +Next, we will create a [submodule](http://www.kernel.org/pub/software/scm/git/docs/git-submodule.html) for the `system` directory. Go to and copy the "Clone URL": + +![Github Clone URL](http://img.skitch.com/20091019-rud5mmqbf776jwua6hx9nm1n.png) + +Now use the URL to create the submodule for `system`: + + git submodule add git://github.com/kohana/core.git system + +[!!] This will create a link to the current development version of the next stable release. The development version should almost always be safe to use, have the same API as the current stable download with bugfixes applied. + +Now add whatever submodules you need. For example if you need the [Database] module: + + git submodule add git://github.com/kohana/database.git modules/database + +After submodules are added, they must be initialized: + + git submodule init + +Now that the submodules are added, you can commit them: + + git commit -m 'Added initial submodules' + +Next, create the application directory structure. This is the bare minimum required: + + mkdir -p application/classes/{controller,model} + mkdir -p application/{config,views} + mkdir -m 0777 -p application/{cache,logs} + +If you run `find application` you should see this: + + application + application/cache + application/config + application/classes + application/classes/controller + application/classes/model + application/logs + application/views + +We don't want git to track log or cache files, so add a `.gitignore` file to each of the directories. This will ignore all non-hidden files: + + echo '[^.]*' > application/{logs,cache}/.gitignore + +[!!] Git ignores empty directories, so adding a `.gitignore` file also makes sure that git will track the directory, but not the files within it. + +Now we need the `index.php` and `bootstrap.php` files: + + wget http://github.com/kohana/kohana/raw/master/index.php + wget http://github.com/kohana/kohana/raw/master/application/bootstrap.php -O application/bootstrap.php + +Commit these changes too: + + git add application + git commit -m 'Added initial directory structure' + +That's all there is to it. You now have an application that is using Git for versioning. + +## Updating Submodules + +At some point you will probably also want to upgrade your submodules. To update all of your submodules to the latest `HEAD` version: + + git submodule foreach 'git checkout master && git pull origin master' + +To update a single submodule, for example, `system`: + + cd system + git checkout master + git pull origin master + cd .. + git add system + git commit -m 'Updated system to latest version' + +If you want to update a single submodule to a specific commit: + + cd modules/database + git pull origin master + git checkout fbfdea919028b951c23c3d99d2bc1f5bbeda0c0b + cd ../.. + git add database + git commit -m 'Updated database module' + +Note that you can also check out the commit at a tagged official release point, for example: + + git checkout 3.0.6 + +Simply run `git tag` without arguments to get a list of all tags. + +All done! diff --git a/includes/kohana/modules/userguide/guide/tutorials.helloworld.md b/includes/kohana/modules/userguide/guide/tutorials.helloworld.md new file mode 100644 index 00000000..ce0c07e0 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/tutorials.helloworld.md @@ -0,0 +1,106 @@ +# Hello, World + +Just about every framework ever written has some kind of hello world example included, so it'd be pretty rude of us to break this tradition! + +We'll start out by creating a very very basic hello world, and then we'll expand it to follow MVC principles. + +## Bare bones + +First off we have to make a controller that Kohana can use to handle a request. + +Create the file `application/classes/controller/hello.php` in your application folder and fill it out like so: + + template->message = 'hello, world!'; + } + } + +`extends Controller_Template` +: We're now extending the template controller, it makes it more convenient to use views within our controller. + +`public $template = 'site';` +: The template controller needs to know what template you want to use. It'll automatically load the view defined in this variable and assign the view object to it. + +`$this->template->message = 'hello, world!';` +: `$this->template` is a reference to the view object for our site template. What we're doing here is assigning a variable called "message", with a value of "hello, world!" to the view. + +Now lets try running our code... + +
          {{userguide/examples/hello_world_error}}
          + +For some reason Kohana's thrown a wobbly and isn't showing our amazing message. + +If we look at the error message we can see that the View library wasn't able to find our site template, probably because we haven't made it yet – *doh*! + +Let's go and make the view file `application/views/site.php` for our message: + + + + We've got a message for you! + + + +

          +

          We just wanted to say it! :)

          + + + +If we refresh the page then we can see the fruits of our labour: + +![hello, world! We just wanted to say it!](img/hello_world_2.png "hello, world! We just wanted to say it!") + +## Stage 3 – Profit! + +In this tutorial you've learnt how to create a controller and use a view to separate your logic from your display. + +This is obviously a very basic introduction to working with Kohana and doesn't even scrape the potential you have when developing applications with it. diff --git a/includes/kohana/modules/userguide/guide/tutorials.orm.md b/includes/kohana/modules/userguide/guide/tutorials.orm.md new file mode 100644 index 00000000..3fef93f4 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/tutorials.orm.md @@ -0,0 +1,298 @@ +# ORM {#top} + +Kohana 3.0 includes a powerful ORM module that uses the active record pattern and database introspection to determine a model's column information. + +The ORM module is included with the Kohana 3.0 install but needs to be enabled before you can use it. In your `application/bootstrap.php` file modify the call to [Kohana::modules] and include the ORM module: + + Kohana::modules(array( + ... + 'orm' => MODPATH.'orm', + ... + )); + +## Configuration {#configuration} + +ORM requires little configuration to get started. Extend your model classes with ORM to begin using the module: + + class Model_User extends ORM + { + ... + } + +In the example above, the model will look for a `users` table in the default database. + +### Model Configuration Properties + +The following properties are used to configure each model: + +Type | Option | Description | Default value +----------|---------------------|----------------------------------| ------------------------- +`string` | _table_name | Table name to use | `singular model name` +`string` | _db | Name of the database to use | `default` +`string` | _primary_key | Column to use as primary key | `id` +`string` | _primary_val | Column to use as primary value | `name` +`bool` | _table_names_plural | Whether tables names are plural | `TRUE` +`array` | _sorting | Array of column => direction | `primary key => ASC` +`string` | _foreign_key_suffix | Suffix to use for foreign keys | `_id` + +## Using ORM + +### Loading a Record + +To create an instance of a model, you can use the [ORM::factory] method or the [ORM::__construct]: + + $user = ORM::factory('user'); + // or + $user = new Model_User(); + +The constructor and factory methods also accept a primary key value to load the given model data: + + // Load user ID 5 + $user = ORM::factory('user', 5); + + // See if the user was loaded successfully + if ($user->loaded()) { ... } + +You can optionally pass an array of key => value pairs to load a data object matching the given criteria: + + // Load user with email joe@example.com + $user = ORM::factory('user', array('email' => 'joe@example.com')); + +### Searching for a Record or Records + +ORM supports most of the [Database] methods for powerful searching of your model's data. See the `_db_methods` property for a full list of supported method calls. Records are retrieved using the [ORM::find] and [ORM::find_all] method calls. + + // This will grab the first active user with the name Bob + $user = ORM::factory('user') + ->where('active', '=', TRUE) + ->where('name', '=', 'Bob') + ->find(); + + // This will grab all users with the name Bob + $users = ORM::factory('user') + ->where('name', '=', 'Bob') + ->find_all(); + +When you are retrieving a list of models using [ORM::find_all], you can iterate through them as you do with database results: + + foreach ($users as $user) + { + ... + } + +A powerful feature of ORM is the [ORM::as_array] method which will return the given record as an array. If used with [ORM::find_all], an array of all records will be returned. A good example of when this is useful is for a select list: + + // Display a select field of usernames (using the id as values) + echo Form::select('user', ORM::factory('user')->find_all()->as_array('id', 'username')); + +### Counting Records + +Use [ORM::count_all] to return the number of records for a given query. + + // Number of users + $count = ORM::factory('user')->where('active', '=', TRUE)->count_all(); + +If you wish to count the total number of users for a given query, while only returning a certain subset of these users, call the [ORM::reset] method with `FALSE` before using `count_all`: + + $user = ORM::factory('user'); + + // Total number of users (reset FALSE prevents the query object from being cleared) + $count = $user->where('active', '=', TRUE)->reset(FALSE)->count_all(); + + // Return only the first 10 of these results + $users = $user->limit(10)->find_all(); + +### Accessing Model Properties + +All model properties are accessible using the `__get` and `__set` magic methods. + + $user = ORM::factory('user', 5); + + // Output user name + echo $user->name; + + // Change user name + $user->name = 'Bob'; + +To store information/properties that don't exist in the model's table, you can use the `_ignored_columns` data member. Data will be stored in the internal `_object` member, but ignored at the database level. + + class Model_User extends ORM + { + ... + protected $_ignored_columns = array('field1', 'field2', ...); + ... + } + +Multiple key => value pairs can be set by using the [ORM::values] method. + + $user->values(array('username' => 'Joe', 'password' => 'bob')); + +### Creating and Saving Records + +The [ORM::save] method is used to both create new records and update existing records. + + // Creating a record + $user = ORM::factory('user'); + $user->name = 'New user'; + $user->save(); + + // Updating a record + $user = ORM::factory('user', 5); + $user->name = 'User 2'; + $user->save(); + + // Check to see if the record has been saved + if ($user->saved()) { ... } + +You can update multiple records by using the [ORM::save_all] method: + + $user = ORM::factory('user'); + $user->name = 'Bob'; + + // Change all active records to name 'Bob' + $user->where('active', '=', TRUE)->save_all(); + +#### Using `Updated` and `Created` Columns + +The `_updated_column` and `_created_column` members are provided to automatically be updated when a model is updated and created. These are not used by default. To use them: + + // date_created is the column used for storing the creation date. Use format => TRUE to store a timestamp. + protected $_created_column = array('column' => 'date_created', 'format' => TRUE); + + // date_modified is the column used for storing the modified date. In this case, a string specifying a date() format is used. + protected $_updated_column = array('column' => 'date_modified', 'format' => 'm/d/Y'); + +### Deleting Records + +Records are deleted with [ORM::delete] and [ORM::delete_all]. These methods operate in the same fashion as saving described above with the exception that [ORM::delete] takes one optional parameter, the `id` of the record to delete. Otherwise, the currently loaded record is deleted. + +### Relationships + +ORM provides for powerful relationship support. Ruby has [a great tutorial on relationships](http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html). + +#### Belongs-To and Has-Many + +We'll assume we're working with a school that has many students. Each student can belong to only one school. You would define the relationships in this manner: + + // Inside the school model + protected $_has_many = array('students' => array()); + + // Inside the student model + protected $_belongs_to = array('school' => array()); + +To access a student's school you use: + + $school = $student->school; + +To access a school's students, you would use: + + // Note that find_all is required after students + $students = $school->students->find_all(); + + // To narrow results: + $students = $school->students->where('active', '=', TRUE)->find_all(); + +By default, ORM will look for a `school_id` model in the student table. This can be overriden by using the `foreign_key` attribute: + + protected $_belongs_to = array('school' => array('foreign_key' => 'schoolID')); + +The foreign key should be overridden in both the student and school models. + +#### Has-One + +Has-One is a special case of Has-Many, the only difference being that there is one and only one record. In the above example, each school would have one and only one student (although this is a poor example). + + // Inside the school model + protected $_has_one = array('student' => array()); + +Like Belongs-To, you do not need to use the `find` method when referencing the Has-One related object - it is done automatically. + +#### Has-Many "Through" + +The Has-Many "through" relationship (also known as Has-And-Belongs-To-Many) is used in the case of one object being related to multiple objects of another type, and visa-versa. For instance, a student may have multiple classes and a class may have multiple students. In this case, a third table and model known as a `pivot` is used. In this case, we will call the pivot object/model `enrollment`. + + // Inside the student model + protected $_has_many = array('classes' => array('through' => 'enrollment')); + + // Inside the class model + protected $_has_many = array('students' => array('through' => 'enrollment')); + +The enrollment table should contain two foreign keys, one for `class_id` and the other for `student_id`. These can be overriden using `foreign_key` and `far_key` when defining the relationship. For example: + + // Inside the student model (the foreign key refers to this model [student], while the far key refers to the other model [class]) + protected $_has_many = array('classes' => array('through' => 'enrollment', 'foreign_key' => 'studentID', 'far_key' => 'classID')); + + // Inside the class model + protected $_has_many = array('students' => array('through' => 'enrollment', 'foreign_key' => 'classID', 'far_key' => 'studentID')); + +The enrollment model should be defined as such: + + // Enrollment model belongs to both a student and a class + protected $_belongs_to = array('student' => array(), 'class' => array()); + +To access the related objects, use: + + // To access classes from a student + $student->classes->find_all(); + + // To access students from a class + $class->students->find_all(); + +### Validation + +ORM is integrated tightly with the [Validate] library. The ORM provides the following members for validation: + +* _rules +* _callbacks +* _filters +* _labels + +#### `_rules` + + protected $_rules = array + ( + 'username' => array('not_empty' => array()), + 'email' => array('not_empty' => array(), 'email' => array()), + ); + +`username` will be checked to make sure it's not empty. `email` will be checked to also ensure it is a valid email address. The empty arrays passed as values can be used to provide optional additional parameters to these validate method calls. + +#### `_callbacks` + + protected $_callbacks = array + ( + 'username' => array('username_unique'), + ); + +`username` will be passed to a callback method `username_unique`. If the method exists in the current model, it will be used, otherwise a global function will be called. Here is an example of the definition of this method: + + public function username_unique(Validate $data, $field) + { + // Logic to make sure a username is unique + ... + } + +#### `_filters` + + protected $_filters = array + ( + TRUE => array('trim' => array()), + 'username' => array('stripslashes' => array()), + ); + +`TRUE` indicates that the `trim` filter is to be used on all fields. `username` will be filtered through `stripslashes` before it is validated. The empty arrays passed as values can be used to provide additional parameters to these filter method calls. + +#### Checking if the Object is Valid + +Use [ORM::check] to see if the object is currently valid. + + // Setting an object's values, then checking to see if it's valid + if ($user->values($_POST)->check()) + { + $user->save(); + } + +You can use the `validate()` method to access the model's validation object. + + // Add an additional filter manually + $user->validate()->filter('username', 'trim'); diff --git a/includes/kohana/modules/userguide/guide/tutorials.removeindex.md b/includes/kohana/modules/userguide/guide/tutorials.removeindex.md new file mode 100644 index 00000000..9c88f0a2 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/tutorials.removeindex.md @@ -0,0 +1,88 @@ +# Removing `index.php` From the URL + +To keep your URLs clean, you will probably want to be able to access your app without having `/index.php/` in the URL. There are two steps to remove `index.php` from the URL. + +1. Edit the bootstrap file +2. Set up rewriting + +# Configure Bootstrap + +The first thing you will need to change is the `index_file` setting of [Kohana::init]: + + Kohana::init(array( + 'base_url' => '/myapp/', + 'index_file' => FALSE, + )); + +Now all of the links generated using [URL::site], [URL::base], and [HTML::anchor] will no longer include "index.php" in the URL. All generated links will start with `/myapp/` instead of `/myapp/index.php/`. + +# URL Rewriting + +Enabling rewriting is done differently, depending on your web server. + +## Apache + +Rename `example.htaccess` to only `.htaccess` and alter the following line of code: + + RewriteBase /kohana/ + +This needs to match the `base_url` setting of [Kohana::init]: + + RewriteBase /myapp/ + +In most cases, this is all you will need to change. + +### Failed! + +If you get a "Internal Server Error" or "No input file specified" error, try changing: + + RewriteRule ^(?:application|modules|system)\b - [F,L] + +Instead, we can try a slash: + + RewriteRule ^(application|modules|system)/ - [F,L] + +If that doesn't work, try changing: + + RewriteRule .* index.php/$0 [PT] + +To something more simple: + + RewriteRule .* index.php [PT] + +### Still Failed! + +If you are still getting errors, check to make sure that your host supports URL `mod_rewrite`. If you can change the Apache configuration, add these lines to the the configuration, usually `httpd.conf`: + + + Order allow,deny + Allow from all + AllowOverride All + + +## NGINX + +It is hard to give examples of nginx configuration, but here is a sample for a server: + + location / { + index index.php index.html index.htm; + try_files $uri $uri/ index.php$uri?$args; + } + + location ~ ^(.+\.php)(.*)$ { + fastcgi_split_path_info ^(.+\.php)(.*)$; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + + include fastcgi.conf; + + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + } + +The two things to note are the use of [try_files](http://wiki.nginx.org/NginxHttpCoreModule#try_files) and [fastcgi_split_path_info](http://wiki.nginx.org/NginxHttpFcgiModule#fastcgi_split_path_info). + +[!!] This assumes that you are running PHP as a FastCGI server on port 9000 and are using nginx v0.7.31 or later. + +If you are having issues getting this working, enable debug level logging in nginx and check the access and error logs. diff --git a/includes/kohana/modules/userguide/guide/tutorials.urls.md b/includes/kohana/modules/userguide/guide/tutorials.urls.md new file mode 100755 index 00000000..b03befac --- /dev/null +++ b/includes/kohana/modules/userguide/guide/tutorials.urls.md @@ -0,0 +1,159 @@ +# Routes, URLs, and Links + +This section will provide you with the basic idea behind Kohana's request routing, url generation and links. + +## Routing + +As mentioned in the [Request Flow](about.flow) section, a request is handled by the [Request] class which finds a matching [Route] and loads the appropriate controller to handle the request. This system provides much flexibility as well as a common sense default behavior. + +If you look in `APPPATH/bootstrap.php` you will see the following code which is run immediately before the request is handed off to [Request::instance]: + + Route::set('default', '((/(/)))') + ->defaults(array( + 'controller' => 'welcome', + 'action' => 'index', + )); + +This sets the `default` route with a uri in the format of `((/(/)))`. The tokens surrounded with `<>` are *keys* and the tokens surrounded with `()` are *optional* parts of the uri. In this case, the entire uri is optional, so a blank uri would match and the default controller and action would be assumed resulting in the `Controller_Welcome` class being loaded and eventually the `action_index` method being called to handle the request. + +Notice that in Kohana routes, any characters are allowed aside from `()<>` and the `/` has no special meaning. In the default route the `/` is used as a static separator but as long as the regex makes sense there is no restriction to how you can format your routes. + +### Directories + +For organizational purposes you may wish to place some of your controllers in subdirectories. A common case is for an admin backend to your site: + + Route::set('admin', 'admin(/(/(/)))') + ->defaults(array( + 'directory' => 'admin', + 'controller' => 'home', + 'action' => 'index', + )); + +This route specifies that the uri must begin with `admin` to match and the directory is statically assigned to `admin` in the defaults. Now a request to `admin/users/create` would load the `Controller_Admin_Users` class and call the `action_create` method. + +### Patterns + +The Kohana route system uses perl compatible regular expressions in its matching process. By default the keys (surrounded by `<>`) are matched by `[a-zA-Z0-9_]++` but you can define your own patterns for each key by passing an associative array of keys and patterns as an additional argument to [Route::set]. To extend our previous example let's say you have an admin section and an affiliates section. You could specify those in separate routes or you could do something like this: + + Route::set('sections', '(/(/(/)))', + array( + 'directory' => '(admin|affiliate)' + )) + ->defaults(array( + 'controller' => 'home', + 'action' => 'index', + )); + +This would provide you with two sections of your site, 'admin' and 'affiliate' which would let you organize the controllers for each into subdirectories but otherwise work like the default route. + +### More Route Examples + +There are countless other possibilities for routes. Here are some more examples: + + /* + * Authentication shortcuts + */ + Route::set('auth', '', + array( + 'action' => '(login|logout)' + )) + ->defaults(array( + 'controller' => 'auth' + )); + + /* + * Multi-format feeds + * 452346/comments.rss + * 5373.json + */ + Route::set('feeds', '(/).', + array( + 'user_id' => '\d+', + 'format' => '(rss|atom|json)', + )) + ->defaults(array( + 'controller' => 'feeds', + 'action' => 'status', + )); + + /* + * Static pages + */ + Route::set('static', '.html', + array( + 'path' => '[a-zA-Z0-9_/]+', + )) + ->defaults(array( + 'controller' => 'static', + 'action' => 'index', + )); + + /* + * You don't like slashes? + * EditGallery:bahamas + * Watch:wakeboarding + */ + Route::set('gallery', '():', + array( + 'controller' => '[A-Z][a-z]++', + 'action' => '[A-Z][a-z]++', + )) + ->defaults(array( + 'controller' => 'Slideshow', + )); + + /* + * Quick search + */ + Route::set('search', ':', array('query' => '.*')) + ->defaults(array( + 'controller' => 'search', + 'action' => 'index', + )); + +Routes are matched in the order specified so be aware that if you set routes after the modules have been loaded a module could specify a route that conflicts with your own. This is also the reason that the default route is set last, so that custom routes will be tested first. + +### Request Parameters + +The directory, controller and action can be accessed from the [Request] instance in either of these two ways: + + $this->request->action; + Request::instance()->action; + +All other keys specified in a route can be accessed from within the controller via: + + $this->request->param('key_name'); + +The [Request::param] method takes an optional second argument to specify a default return value in case the key is not set by the route. If no arguments are given, all keys are returned as an associative array. + +### Convention + +The established convention is to either place your custom routes in the `MODPATH//init.php` file of your module if the routes belong to a module, or simply insert them into the `APPPATH/bootstrap.php` file above the default route if they are specific to the application. Of course, they could also be included from an external file or even generated dynamically. + +## URLs + +Along with Kohana's powerful routing capabilities are included some methods for generating URLs for your routes' uris. You can always specify your uris as a string using [URL::site] to create a full URL like so: + + URL::site('admin/edit/user/'.$user_id); + +However, Kohana also provides a method to generate the uri from the route's definition. This is extremely useful if your routing could ever change since it would relieve you from having to go back through your code and change everywhere that you specified a uri as a string. Here is an example of dynamic generation that corresponds to the `feeds` route example from above: + + Route::get('feeds')->uri(array( + 'user_id' => $user_id, + 'action' => 'comments', + 'format' => 'rss' + )); + +Let's say you decided later to make that route definition more verbose by changing it to `feeds/(/).`. If you wrote your code with the above uri generation method you wouldn't have to change a single line! When a part of the uri is enclosed in parentheses and specifies a key for which there in no value provided for uri generation and no default value specified in the route, then that part will be removed from the uri. An example of this is the `(/)` part of the default route; this will not be included in the generated uri if an id is not provided. + +One method you might use frequently is the shortcut [Request::uri] which is the same as the above except it assumes the current route, directory, controller and action. If our current route is the default and the uri was `users/list`, we can do the following to generate uris in the format `users/view/$id`: + + $this->request->uri(array('action' => 'view', 'id' => $user_id)); + +Or if within a view, the preferable method is: + + Request::instance()->uri(array('action' => 'view', 'id' => $user_id)); + +## Links + +[!!] links stub diff --git a/includes/kohana/modules/userguide/guide/using.autoloading.md b/includes/kohana/modules/userguide/guide/using.autoloading.md new file mode 100644 index 00000000..11c9f517 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/using.autoloading.md @@ -0,0 +1,95 @@ +# Loading Classes + +Kohana takes advantage of PHP [autoloading](http://php.net/manual/language.oop5.autoload.php). This removes the need to call [include](http://php.net/include) or [require](http://php.net/require) before using a class. For instance, when you want to use the [Cookie::set] method, you just call: + + Cookie::set('mycookie', 'any string value'); + +Or to load an [Encrypt] instance, just call [Encrypt::instance]: + + $encrypt = Encrypt::instance(); + +Classes are loaded via the [Kohana::auto_load] method, which makes a simple conversion from class name to file name: + +1. Classes are placed in the `classes/` directory of the [filesystem](about.filesystem) +2. Any underscore characters are converted to slashes +2. The filename is lowercase + +When calling a class that has not been loaded (eg: `Session_Cookie`), Kohana will search the filesystem using [Kohana::find_file] for a file named `classes/session/cookie.php`. + +## Custom Autoloaders + +The default autoloader is enabled in `application/bootstrap.php` using [spl_autoload_register](http://php.net/spl_autoload_register): + + spl_autoload_register(array('Kohana', 'auto_load')); + +This allows [Kohana::auto_load] to attempt to load any class that does not yet exist when the class is first used. + +# Transparent Class Extension {#class-extension} + +The [cascading filesystem](about.filesystem) allows transparent class extension. For instance, the class [Cookie] is defined in `SYSPATH/classes/cookie.php` as: + + class Cookie extends Kohana_Cookie {} + +The default Kohana classes, and many extensions, use this definition so that almost all classes can be extended. You extend any class transparently, by defining your own class in `APPPATH/classes/cookie.php` to add your own methods. + +[!!] You should **never** modify any of the files that are distributed with Kohana. Always make modifications to classes using extensions to prevent upgrade issues. + +For instance, if you wanted to create method that sets encrypted cookies using the [Encrypt] class: + + encode((string) $value); + + parent::set($name, $value, $expiration); + } + + /** + * Gets an encrypted cookie. + * + * @uses Cookie::get + * @uses Encrypt::decode + */ + public static function decrypt($name, $default = NULL) + { + if ($value = parent::get($name, NULL)) + { + $value = Encrypt::instance(Cookie::$encryption)->decode($value); + } + + return isset($value) ? $value : $default; + } + + } // End Cookie + +Now calling `Cookie::encrypt('secret', $data)` will create an encrypted cookie which we can decrypt with `$data = Cookie::decrypt('secret')`. + +## Multiple Levels of Extension {#multiple-extensions} + +If you are extending a Kohana class in a module, you should maintain transparent extensions. Instead of making the [Cookie] extension extend Kohana, you can create `MODPATH/mymod/encrypted/cookie.php`: + + class Encrypted_Cookie extends Kohana_Cookie { + + // Use the same encrypt() and decrypt() methods as above + + } + +And create `MODPATH/mymod/cookie.php`: + + class Cookie extends Encrypted_Cookie {} + +This will still allow users to add their own extension to [Cookie] with your extensions intact. However, the next extension of [Cookie] will have to extend `Encrypted_Cookie` instead of `Kohana_Cookie`. diff --git a/includes/kohana/modules/userguide/guide/using.configuration.md b/includes/kohana/modules/userguide/guide/using.configuration.md new file mode 100644 index 00000000..4c29a650 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/using.configuration.md @@ -0,0 +1,57 @@ +# General Configuration + +Kohana uses both static properties and files for configuration. Static properties are typically used for static classes, such as [Cookie], [Security], and [Upload]. Files are typically used for objects such as [Database], [Encrypt], and [Session]. + +Static properties can be set in `APPPATH/bootstrap.php` or by [class extension](using.autoloading#class-extension). The benefit of static properties is that no additional files need to be loaded. The problem with this method is that it causes the class to be loaded when the property is set, if you do not use an extension. However, using extensions will overload extensions made in modules. It is generally recommended to do static property configuration in the bootstrap. + +[!!] When using opcode caching, such as [APC](http://php.net/apc) or [eAccelerator](http://eaccelerator.net/), class loading time is significantly reduced. It is highly recommended to use opcode caching with *any* production website, no matter the size. + +## Initial Settings + +Every new Kohana installation will require changing [Kohana::init] settings in `APPPATH/bootstrap.php`. Any setting that is not set will use the default setting. These settings can be accessed and modified later by using the static property of the [Kohana] class. For instance, to get the current character set, read the [Kohana::$charset] property. + +## Security Settings + +There are several settings which need to be changed to make Kohana secure. The most important of these is [Cookie::$salt], which is used to create a "signature" on cookies that prevents them from being modified outside of Kohana. + +If you plan to use the [Encrypt] class, you will also need to create an `encrypt` configuration file and set the encryption `key` value. The encryption key should include letters, numbers, and symbols for the best security. + +[!!] **Do not use a hash for the encryption key!** Doing so will make the encryption key much easier to crack. + +# Configuration Files {#config-files} + +Configuration files are slightly different from other files within the [cascading filesystem](about.filesystem) in that they are **merged** rather than overloaded. This means that all configuration files with the same file path are combined to produce the final configuration. The end result is that you can overload *individual* settings rather than duplicating an entire file. + +Configuration files are plain PHP files, stored in the `config/` directory, which return an associative array: + + 'value', + 'options' => array( + 'foo' => 'bar', + ), + ); + +If the above configuration file was called `myconf.php`, you could access it using: + + $config = Kohana::config('myconf'); + $options = $config['options']; + +[Kohana::config] also provides a shortcut for accessing individual keys from configuration arrays using "dot paths". + +Get the "options" array: + + $options = Kohana::config('myconf.options'); + +Get the "foo" key from the "options" array: + + $foo = Kohana::config('myconf.options.foo'); + +Configuration arrays can also be accessed as objects, if you prefer that method: + + $options = Kohana::config('myconf')->options; + +Please note that you can only access the top level of keys as object properties, all child keys must be accessed using standard array syntax: + + $foo = Kohana::config('myconf')->options['foo']; diff --git a/includes/kohana/modules/userguide/guide/using.messages.md b/includes/kohana/modules/userguide/guide/using.messages.md new file mode 100644 index 00000000..896daede --- /dev/null +++ b/includes/kohana/modules/userguide/guide/using.messages.md @@ -0,0 +1,26 @@ +# Message Basics + +Kohana messages are human friendly strings represented by a shorter word or phrase, called a "key". Messages are accessed using the [Kohana::message] method, which returns either an entire group of messages, or a single message. + +As an example, when a user is not logged in and attempts to access a page that requires authentication, an error such as "You must be logged in to access this page" might be displayed. This message could be stored in the `auth` file with a `must_login` key: + + $message = Kohana::message('auth', 'must_login'); + +Messages are not translated. To translate a message, use the [translation function](using.translation): + + $translated = __(Kohana::message('auth', 'must_login')); + +[!!] In Kohana v2, the message system was used for translation. However, it is highly recommended to use the new translation system instead of messages, as it provides readable text even when a translation is not available. + +## Message Files + +All message files are plain PHP files, stored in the `messages/` directory, that return an associative array: + + 'You must login to access this page', + 'no_access' => 'You do not have privileges to access this page', + ); + +Message files are similar to [config files](using.configuration#config-files) in that they are merged together. This means that all of the messages stored in a file called `auth` will be combined into a single array, so it is not necessary to duplicate all of the messages when you create a new `auth` file. diff --git a/includes/kohana/modules/userguide/guide/using.sessions.md b/includes/kohana/modules/userguide/guide/using.sessions.md new file mode 100644 index 00000000..f2cefda2 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/using.sessions.md @@ -0,0 +1,223 @@ +# Using Sessions and Cookies + +Kohana provides a couple of classes that make it easy to work with both cookies and sessions. At a high level both sessions and cookies provide the same function. They allow the developer to store temporary or persistent information about a specific client for later retrieval. + +Cookies should be used for storing non-private data that is persistent for a long period of time. For example storing a user id or a language preference. Use the [Cookie] class for getting and setting cookies. + +[!!] Kohana uses "signed" cookies. Every cookie that is stored is combined with a secure hash to prevent modification of the cookie. This hash is generated using [Cookie::salt], which uses the [Cookie::$salt] property. You should [change this setting](using.configuration) when your application is live. + +Sessions should be used for storing temporary or private data. Very sensitive data should be stored using the [Session] class with the "database" or "native" adapters. When using the "cookie" adapter, the session should always be encrypted. + +[!!] For more information on best practices with session variables see [the seven deadly sins of sessions](http://lists.nyphp.org/pipermail/talk/2006-December/020358.html). + +# Storing, Retrieving, and Deleting Data + +[Cookie] and [Session] provide a very similar API for storing data. The main difference between them is that sessions are accessed using an object, and cookies are accessed using a static class. + +Accessing the session instance is done using the [Session::instance] method: + + // Get the session instance + $session = Session::instance(); + +When using sessions, you can also get all of the current session data using the [Session::as_array] method: + + // Get all of the session data as an array + $data = $session->as_array(); + +You can also use this to overload the `$_SESSION` global to get and set data in a way more similar to standard PHP: + + // Overload $_SESSION with the session data + $_SESSION =& $session->as_array(); + + // Set session data + $_SESSION[$key] = $value; + +## Storing Data {#setting} + +Storing session or cookie data is done using the `set` method: + + // Set session data + $session->set($key, $value); + + // Set cookie data + Cookie::set($key, $value); + + // Store a user id + $session->set('user_id', 10); + Cookie::set('user_id', 10); + +## Retrieving Data {#getting} + +Getting session or cookie data is done using the `get` method: + + // Get session data + $data = $session->get($key, $default_value); + + // Get cookie data + $data = Cookie::get($key, $default_value); + + // Get the user id + $user = $session->get('user_id'); + $user = Cookie::get('user_id'); + +## Deleting Data {#deleting} + +Deleting session or cookie data is done using the `delete` method: + + // Delete session data + $session->delete($key); + + // Delete cookie data + Cookie::delete($key); + + // Delete the user id + $session->delete('user_id'); + Cookie::delete('user_id'); + +# Configuration {#configuration} + +Both cookies and sessions have several configuration settings which affect how data is stored. Always check these settings before making your application live, as many of them will have a direct affect on the security of your application. + +## Cookie Settings {#cookie-settings} + +All of the cookie settings are changed using static properties. You can either change these settings in `bootstrap.php` or by using a [class extension](using.autoloading#class-extension). + +The most important setting is [Cookie::$salt], which is used for secure signing. This value should be changed and kept secret: + + Cookie::$salt = 'your secret is safe with me'; + +[!!] Changing this value will render all cookies that have been set before invalid. + +By default, cookies are stored until the browser is closed. To use a specific lifetime, change the [Cookie::$expiration] setting: + + // Set cookies to expire after 1 week + Cookie::$expiration = 604800; + + // Alternative to using raw integers, for better clarity + Cookie::$expiration = Date::WEEK; + +The path that the cookie can be accessed from can be restricted using the [Cookie::$path] setting. + + // Allow cookies only when going to /public/* + Cookie::$path = '/public/'; + +The domain that the cookie can be accessed from can also be restricted, using the [Cookie::$domain] setting. + + // Allow cookies only on the domain www.example.com + Cookie::$domain = 'www.example.com'; + +If you want to make the cookie accessible on all subdomains, use a dot at the beginning of the domain. + + // Allow cookies to be accessed on example.com and *.example.com + Cookie::$domain = '.example.com'; + +To only allow the cookie to be accessed over a secure (HTTPS) connection, use the [Cookie::$secure] setting. + + // Allow cookies to be accessed only on a secure connection + Cookie::$secure = TRUE; + + // Allow cookies to be accessed on any connection + Cookie::$secure = FALSE; + +To prevent cookies from being accessed using Javascript, you can change the [Cookie::$httponly] setting. + + // Make cookies inaccessible to Javascript + Cookie::$httponly = TRUE; + +## Session Adapters {#adapters} + +When creating or accessing an instance of the [Session] class you can decide which session adapter you wish to use. The session adapters that are available to you are: + +Native +: Stores session data in the default location for your web server. The storage location is defined by [session.save_path](http://php.net/manual/session.configuration.php#ini.session.save-path) in `php.ini` or defined by [ini_set](http://php.net/ini_set). + +Database +: Stores session data in a database table using the [Session_Database] class. Requires the [Database] module to be enabled. + +Cookie +: Stores session data in a cookie using the [Cookie] class. **Sessions will have a 4KB limit when using this adapter.** + +The default adapter can be set by changing the value of [Session::$default]. The default adapter is "native". + +[!!] As with cookies, a "lifetime" setting of "0" means that the session will expire when the browser is closed. + +### Session Adapter Settings + +You can apply configuration settings to each of the session adapters by creating a session config file at `APPPATH/config/session.php`. The following sample configuration file defines all the settings for each adapter: + + return array( + 'native' => array( + 'name' => 'session_name', + 'lifetime' => 43200, + ), + 'cookie' => array( + 'name' => 'cookie_name', + 'encrypted' => TRUE, + 'lifetime' => 43200, + ), + 'database' => array( + 'name' => 'cookie_name', + 'encrypted' => TRUE, + 'lifetime' => 43200, + 'group' => 'default', + 'table' => 'table_name', + 'columns' => array( + 'session_id' => 'session_id', + 'last_active' => 'last_active', + 'contents' => 'contents' + ), + 'gc' => 500, + ), + ); + +#### Native Adapter {#adapter-native} + +Type | Setting | Description | Default +----------|-----------|---------------------------------------------------|----------- +`string` | name | name of the session | `"session"` +`integer` | lifetime | number of seconds the session should live for | `0` + +#### Cookie Adapter {#adapter-cookie} + +Type | Setting | Description | Default +----------|-----------|---------------------------------------------------|----------- +`string` | name | name of the cookie used to store the session data | `"session"` +`boolean` | encrypted | encrypt the session data using [Encrypt]? | `FALSE` +`integer` | lifetime | number of seconds the session should live for | `0` + +#### Database Adapter {#adapter-database} + +Type | Setting | Description | Default +----------|-----------|---------------------------------------------------|----------- +`string` | group | [Database::instance] group name | `"default"` +`string` | table | table name to store sessions in | `"sessions"` +`array` | columns | associative array of column aliases | `array` +`integer` | gc | 1:x chance that garbage collection will be run | `500` +`string` | name | name of the cookie used to store the session data | `"session"` +`boolean` | encrypted | encrypt the session data using [Encrypt]? | `FALSE` +`integer` | lifetime | number of seconds the session should live for | `0` + +##### Table Schema + +You will need to create the session storage table in the database. This is the default schema: + + CREATE TABLE `sessions` ( + `session_id` VARCHAR(24) NOT NULL, + `last_active` INT UNSIGNED NOT NULL, + `contents` TEXT NOT NULL, + PRIMARY KEY (`session_id`), + INDEX (`last_active`) + ) ENGINE = MYISAM; + +##### Table Columns + +You can change the column names to match an existing database schema when connecting to a legacy session table. The default value is the same as the key value. + +session_id +: the name of the "id" column + +last_active +: UNIX timestamp of the last time the session was updated + +contents +: session data stored as a serialized string, and optionally encrypted diff --git a/includes/kohana/modules/userguide/guide/using.views.md b/includes/kohana/modules/userguide/guide/using.views.md new file mode 100644 index 00000000..e29f9c02 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/using.views.md @@ -0,0 +1,120 @@ +# Using Views + +Views are files that contain the display information for your application. This is most commonly HTML, CSS and Javascript but can be anything you require such as XML or JSON for AJAX output. The purpose of views is to keep this information separate from your application logic for easy reusability and cleaner code. + +While this is true, views themselves can contain code used for displaying the data you pass into them. For example, looping through an array of product information and display each one on a new table row. Views are still PHP files so you can use any code you normally would. + +# Creating View Files + +View files are stored in the `views` directory of the [filesystem](about.filesystem). You can also create sub-directories within the `views` directory to organize your files. All of the following examples are reasonable view files: + + APPPATH/views/home.php + APPPATH/views/pages/about.php + APPPATH/views/products/details.php + MODPATH/error/views/errors/404.php + MODPATH/common/views/template.php + +## Loading Views + +[View] objects will typically be created inside a [Controller] using the [View::factory] method. Typically the view is then assigned as the [Request::$response] property or to another view. + + public function action_about() + { + $this->request->response = View::factory('pages/about'); + } + +When a view is assigned as the [Request::$response], as in the example above, it will automatically be rendered when necessary. To get the rendered result of a view you can call the [View::render] method or just type cast it to a string. When a view is rendered, the view file is loaded and HTML is generated. + + public function action_index() + { + $view = View::factory('pages/about'); + + // Render the view + $about_page = $view->render(); + + // Or just type cast it to a string + $about_page = (string) $view; + + $this->request->response = $about_page; + } + +## Variables in Views + +Once view has been loaded, variables can be assigned to it using the [View::set] and [View::bind] methods. + + public function action_roadtrip() + { + $view = View::factory('user/roadtrip') + ->set('places', array('Rome', 'Paris', 'London', 'New York', 'Tokyo')); + ->bind('user', $this->user); + + // The view will have $places and $user variables + $this->request->response = $view; + } + +[!!] The only difference between `set()` and `bind()` is that `bind()` assigns the variable by reference. If you `bind()` a variable before it has been defined, the variable will be created as `NULL`. + +### Global Variables + +An application may several view files that need access to the same variables. For example, to display a page title in both the header of your template and in the body of the page content. You can create variables that are accessible in any view using the [View::set_global] and [View::bind_global] methods. + + // Assign $page_title to all views + View::bind_global('page_title', $page_title); + +If the application has three views that are rendered for the home page: `template`, `template/sidebar`, and `pages/home`. First, an abstract controller to create the template will be created: + + abstract class Controller_Website extends Controller_Template { + + public $page_title; + + public function before() + { + parent::before(); + + // Make $page_title available to all views + View::bind_global('page_title', $this->page_title); + + // Load $sidebar into the template as a view + $this->template->sidebar = View::factory('template/sidebar'); + } + + } + +Next, the home controller will extend `Controller_Website`: + + class Controller_Home extends Controller_Website { + + public function action_index() + { + $this->page_title = 'Home'; + + $this->template->content = View::factory('pages/home'); + } + + } + +## Views Within Views + +If you want to include another view within a view, there are two choices. By calling [View::factory] you can sandbox the included view. This means that you will have to provide all of the variables to the view using [View::set] or [View::bind]: + + // Only the $user variable will be available in "views/user/login.php" + bind('user', $user) ?> + +The other option is to include the view directly, which makes all of the current variables available to the included view: + + // Any variable defined in this view will be included in "views/message.php" + + +Of course, you can also load an entire [Request] within a view: + + execute() ?> + +This is an example of [HMVC](about.mvc), which makes it possible to create and read calls to other URLs within your application. + +# Upgrading From v2.x + +Unlike version 2.x of Kohana, the view is not loaded within the context of +the [Controller], so you will not be able to access `$this` as the controller +that loaded the view. Passing the controller to the view must be done explictly: + + $view->bind('controller', $this); diff --git a/includes/kohana/modules/userguide/guide/zh-cn/about.conventions.md b/includes/kohana/modules/userguide/guide/zh-cn/about.conventions.md new file mode 100644 index 00000000..e0d360c1 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/about.conventions.md @@ -0,0 +1,303 @@ +# 约定 + +鼓励大家遵循 Kohana 的编码样式,Kohana 基于 [BSD/Allman style](http://en.wikipedia.org/wiki/Indent_style#BSD.2FAllman_style) 的编码样式(这里还有一些更多[关于 Kohana 编码样式的描述](http://dev.kohanaframework.org/wiki/kohana2/CodingStyle)) + +## 类名和文件位置 {#classes} + +在 Kohana 系统中类名严格遵循命名约定才能够[自动加载](using.autoloading)。类名的首字母必须大写,且使用下划线连接单词,千万要注意下划线的重要性,因为它直接关系到文件在文件系统中所存放的位置。 + +请遵循以下约定: + +1. 类名不允许使用骆驼命名法,除非需要创建新一级的目录文件。 +2. 所有的类文件的文件名和目录名都必须是小写。 +3. 所有的类文件都应该存放在 `classes` 目录下面,它可以是在[级联文件系统](about.filesystem)的任何一级。 + +[!!] 不像 Kohana v2.x,这里不再区分 "controllers","models","libraries" 和 "helpers" 文件夹。所有的类都存放在 "classes/" 目录,既可以是完全静态的 辅助函数("helpers")或对象形式的类库("libraries")。你可以使用任意形式的设计模式的类库:静态,单例,适配器等。 + +## 实例 + +请大家记着一点在类文件中,类名到下划线意味着是一个新的目录,参考下面例子: + +类名 | 文件路径 +----------------------|------------------------------- +Controller_Template | classes/controller/template.php +Model_User | classes/model/user.php +Database | classes/database.php +Database_Query | classes/database/query.php +Form | classes/form.php + +## 编码标准 {#coding_standards} + +In order to produce highly consistent source code, we ask that everyone follow the coding standards as closely as possible. + +### Brackets +Please use [BSD/Allman Style](http://en.wikipedia.org/wiki/Indent_style#BSD.2FAllman_style) bracketing. + +### 命名约定 + +Kohana 使用下划线连接命名,而不是驼峰命名。 + +#### 类 + + // 库,使用 _Core 作后缀 + class Beer_Core { + + // 库的继承不需要使用后缀 + class Beer extends Beer_Core + + // 控制器类,使用 _Controller 作后缀 + class Apple_Controller extends Controller { + + // 模型类,使用 _Model 作后缀 + class Cheese_Model extends Model { + + // 辅助类 + class peanut { + +当你实例化一个不需要附带参数的类时不需要使用圆括号: + + // 正确: + $db = new Database; + + // 错误: + $db = new Database(); + +#### 函数和方法 + +函数尽量全小写,并使用下划线分割单词: + + function drink_beverage($beverage) + { + +#### 变量 + +所有变量尽量全小写,并使用下划线分割单词而不是驼峰: + + // 正确: + $foo = 'bar'; + $long_example = 'uses underscores'; + + // 错误: + $weDontWantThis = 'understood?'; + +### 缩进 + +代码在逻辑上缩进使用制表符(TAB)代替空格。 + +垂直间距(即多行)使用空格。制表符并不适用于垂直间距主要是因为不同的人可能设置类不同的制表符宽度。 + + $text = 'this is a long text block that is wrapped. Normally, we aim for ' + . 'wrapping at 80 chars. Vertical alignment is very important for ' + . 'code readability. Remember that all indentation is done with tabs,' + . 'but vertical alignment should be completed with spaces, after ' + . 'indenting with tabs.'; + +### 字符串连接 + +不要在连接符左右使用空格: + + // 正确: + $str = 'one'.$var.'two'; + + // 错误: + $str = 'one'. $var .'two'; + $str = 'one' . $var . 'two'; + +### 单行表达式 + +单行 IF 表达式仅用于破坏正常执行的情况(比如,return 或 continue): + + // 可接受: + if ($foo == $bar) + return $foo; + + if ($foo == $bar) + continue; + + if ($foo == $bar) + break; + + if ($foo == $bar) + throw new Exception('You screwed up!'); + + // 不可接受: + if ($baz == $bun) + $baz = $bar + 2; + +### 比较操作 + +使用 OR 和 AND 作为比较符: + + // 正确: + if (($foo AND $bar) OR ($b AND $c)) + + // 错误: + if (($foo && $bar) || ($b && $c)) + +if/else Blocks + +使用 elseif 而不是 else if: + + // 正确: + elseif ($bar) + + // 错误: + else if($bar) + +### Switch 结构 + +每个 case,break 和 default 都应该是独立的一行。每个 case 或 default 里面必须使用一个制表符(TAB)。 + + switch ($var) + { + case 'bar': + case 'foo': + echo 'hello'; + break; + case 1: + echo 'one'; + break; + default: + echo 'bye'; + break; + } + +### 括号 + +There should be one space after statement name, followed by a parenthesis. The ! (bang) character must have a space on either side to ensure maximum readability. Except in the case of a bang or type casting, there should be no whitespace after an opening parenthesis or before a closing parenthesis. + + // 正确: + if ($foo == $bar) + if ( ! $foo) + + // 错误: + if($foo == $bar) + if(!$foo) + if ((int) $foo) + if ( $foo == $bar ) + if (! $foo) + +### 三元操作 + +所有的三元操作都应该遵循一种标准格式。表达式左右使用括号,而变量则不需要。 + + $foo = ($bar == $foo) ? $foo : $bar; + $foo = $bar ? $foo : $bar; + +所有的比较和操作都必须使用括号括起来作为一个组: + + $foo = ($bar > 5) ? ($bar + $foo) : strlen($bar); + +分离复杂的三元操作(三元的第一部分超过了 80 个字符)为多行形式。spaces should be used to line up operators, which should be at the front of the successive lines: + + $foo = ($bar == $foo) + ? $foo + : $bar; + +### 强制类型转换 + +强制类型转换需要在两边使用空格: + + // 正确: + $foo = (string) $bar; + if ( (string) $bar) + + // 错误: + $foo = (string)$bar; + +如果可能,请使用强制类型转换,而不是三元操作: + + // 正确: + $foo = (bool) $bar; + + // 错误: + $foo = ($bar == TRUE) ? TRUE : FALSE; + +如果强制类型转换整形(int)或布尔型(boolean),请使用短格式: + + // 正确: + $foo = (int) $bar; + $foo = (bool) $bar; + + // 错误: + $foo = (integer) $bar; + $foo = (boolean) $bar; + +### 常量 + +常量尽量使用全大写: + + // 正确: + define('MY_CONSTANT', 'my_value'); + $a = TRUE; + $b = NULL; + + // 错误: + define('MyConstant', 'my_value'); + $a = True; + $b = null; + +请把常量放在比较符号的末端: + + // 正确: + if ($foo !== FALSE) + + // 错误: + if (FALSE !== $foo) + +这是一个略有争议的选择,所以我会解释其理由。如果我们用简单的英语写前面的例子中,正确的例子如下: + + if variable $foo is not exactly FALSE + +但是错误的例子可以理解为: + + if FALSE is not exactly variable $foo + +由于我们是从左向右读,因此把常量放在第一位根本没有意义。 + +### 注解 + +#### 单行注解 + +单行注解使用 //,或许你在使用下面几种注解方式。请在注解符后面保留一个空格在添加注解。坚决不能使用 #。 + + // 正确 + + //错误 + // 错误 + # 错误 + +### 正则表达式 + +如果编码中使用到正则表达式,请尽量使用 PCRE 风格而不是 POSIX 风格。相比较而言 PCRE 风格更为强大,速度更快。 + + // 正确: + if (preg_match('/abc/i'), $str) + + // 错误: + if (eregi('abc', $str)) + +正则表达式使用单引号括起来而不是双引号。单引号的字符串简单而且解析起来更快。 +Unlike double-quoted strings they don't support variable interpolation +nor integrated backslash sequences like \n or \t, etc. + + // 正确: + preg_match('/abc/', $str); + + // 错误: + preg_match("/abc/", $str); + +当需要使用正则搜索活替换时,请使用 $n 符号作反向引用,它的效率优于 \\n。 + + // 正确: + preg_replace('/(\d+) dollar/', '$1 euro', $str); + + // 错误: + preg_replace('/(\d+) dollar/', '\\1 euro', $str); + +最后,请注意如果使用 $ 符号匹配字符串末尾是否允许后换行符的话,如果需要可以附加 D 修饰符解决此问题。[更多详情](http://blog.php-security.org/archives/76-Holes-in-most-preg_match-filters.html)。 + + $str = "email@example.com\n"; + + preg_match('/^.+@.+$/', $str); // TRUE + preg_match('/^.+@.+$/D', $str); // FALSE diff --git a/includes/kohana/modules/userguide/guide/zh-cn/about.filesystem.md b/includes/kohana/modules/userguide/guide/zh-cn/about.filesystem.md new file mode 100644 index 00000000..72793ecc --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/about.filesystem.md @@ -0,0 +1,75 @@ +# 级联文件系统 + +Kohana 文件系统单一的目录结构。 +当使用 [Kohana::find_file] 加载一个文件时,系统会以下顺序搜索: + +Application 路径 +: 在 `index.php` 文件中常量被定义为 `APPPATH`,默认值是 `application`。 + +Module 路径 +: 这是在 `APPPATH/bootstrap.php` 文件中使用 [Kohana::modules] 设置的一组数组。 + 数组的每个值都会按照顺序搜索并添加进来。 + +System 路径 +: 在 `index.php` 文件中常量被定义为 `SYSPATH`。默认值是 `system`。 +所有 “core” 核心文件和类文件都在这里定义。 + +目录中的文件包含了优先顺序建立的从高到低的优先级,这就有可能使得具有"高等级"目录的同名文件的会重载任何可以低于它的文件内容。 + +![级联文件系统示意图](img/cascading_filesystem.png) + +如果在 `APPPATH/views` 目录和 `APPPATH/views` 目录均有一个名为 `welcome.php` 视图文件, +当 `welcome.php` 被加载的时候由于 application 目录在文件系统的最上面所以只有它会被返回。 + +## 文件类型 + +目录的级别从高到低依次是 application,module 和 system 路径,分别都有下面的目录结构: + +classes/ +: 所有你想要 [autoload](using.autoloading) 的类库均保存在这里。 + 本目录包含了控制器,模型和其他类库。所有的库文件都必须遵循[类的命名规则](about.conventions#classes)。 + +config/ +: 配置文件是使用 [Kohana::config] 返回的数组项。 + 详情请查阅[配置的用法](using.configuration)。 + +i18n/ +: 多语言文件返回的包各国语言的字符串数组。多语言是使用 `__()` 方法实现。 + 如果想把 "Hello, world" 多语言化,只需要调用 `__('Hello, world!')` 并设置 + [I18n::$lang] 为 "zh-cn"。 + 详情请查阅[多语言的用法](using.translation)。 + +messages/ +: 消息文件是使用 [Kohana::message] 返回的字符串数组。消息和 i18n 文件唯一不同的就是无法多语言化, + 但是总是携程默认语言并通过单键引用。 + 详情请查阅[消息的用法](using.messages)。 + +views/ +: 视图是标准的 PHP 文件被用于生成 HTML。视图文件被加载到 [View] 对象中并得到变量的设置, + 最后在转换为 HTML 片段或其他输出。多个视图可以相互引用。 + 详情请查阅[视图的用法](using.views)。 + +## 查找文件 + +使用 [Kohana::find_file] 方法可以找到在文件系统中任意路径下的文件: + + // 查询的路径 "classes/cookie.php" + $path = Kohana::find_file('classes', 'cookie'); + + // 查询的路径 "views/user/login.php" + $path = Kohana::find_file('views', 'user/login'); + + +## 第三方扩展 + +调用扩展并非限定在 Kohana 。 +比如,如果你想使用 [DOMPDF](http://code.google.com/p/dompdf), +只需把他复制到 `application/vendor/dompdf` 并加载 DOMPDF 的自动加载类: + + require Kohana::find_file('vendor', 'dompdf/dompdf/dompdf_config.inc'); + +现在无需再加载任何文件就可以使用 DOMPDF: + + $pdf = new DOMPDF; + +[!!] 如果你想使用 DOMPDF 转换试图到 PDFs,可以试试 [PDFView](http://github.com/shadowhand/pdfview) 扩展。 diff --git a/includes/kohana/modules/userguide/guide/zh-cn/about.flow.md b/includes/kohana/modules/userguide/guide/zh-cn/about.flow.md new file mode 100644 index 00000000..2c5ddc69 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/about.flow.md @@ -0,0 +1,73 @@ +# 请求流程 + +每个应用都遵循同样的流程: + +1. 程序从 `index.php` 文件加载 +2. 设置 application,module 和 system 目录到路径 +3. 设置错误报告的级别 +4. 加载 install.php 文件(如果存在的话) +5. 加载 [Kohana] 类 +6. 加载 `APPPATH/bootstrap.php` 引导文件 +7. 调用 [Kohana::init] 方法初始化错误句柄,缓存和日志设置 +8. 设置 [Kohana_Config] 读取器和 [Kohana_Log] 记录器 +9. 调用 [Kohana::modules] 方法加载激活的模块 + * 模块路径附加到[文件级联系统](about.filesystem). + * 加载模块的 `init.php` 文件(如果存在的话) + * `init.php` 文件可以增强系统环境设置,同时也包括路由 +10. [Route::set] 会被多次调用来定义[程序路由](using.routing) +11. 调用 [Request::instance] 方法来处理请求 + 1. 检测每个路由直到发现匹配的 + 2. 加载控制器实例化并传递请求 + 3. 调用 [Controller::before] 方法 + 4. 调用控制器的方法生成请求的响应 + 5. 调用 [Controller::after] 方法 + * 当使用 [HMVC sub-requests](about.mvc) 时以上五步会多次循环调用 +12. 显示 [Request] 响应 + +## index.php + +Kohana 遵循[前端控制器]模式,因此所有的请求都要发送到 `index.php` 文件。这样就可以允许保持一个非常整洁的[文件系统](about.filesystem)设计。在 `index.php` 文件中有一些非常重要而又基础的配置变量。你可以改变 `$application`,`$modules` 和 `$system` 的路径以及设置错误报告级别。 + +`$application` 变量让目录包含着你的程序文件。默认情况下,就是 `application` 目录。`$modules` 变量让目录包含着你的扩展文件。默认情况下。`$system` 变量让目录包含着默认的 Kohana 文件。默认情况下。 + +你可以移动下面三个目录到任意路径。假如你的目录结构是: + + www/ + index.php + application/ + modules/ + system/ + +你想转移这些目录到 web 目录以外: + + application/ + modules/ + system/ + www/ + index.php + +那么你应该在 `index.php` 文件改变下面变量的配置: + + $application = '../application'; + $modules = '../modules'; + $system = '../system'; + +Now none of the directories can be accessed by the web server. It is not necessary to make this change, but does make it possible to share the directories with multiple applications, among other things. + +[!!] There is a security check at the top of every Kohana file to prevent it from being accessed without using the front controller. However, it is more secure to move the application, modules, and system directories to a location that cannot be accessed via the web. + +### 错误报告 + +默认情况下,Kohana显示所有错误,包括严格的警告。 + + error_reporting(E_ALL | E_STRICT); + +对于已经上线并在运行的程序,一个保守的推荐,可以忽略掉提醒: + + error_reporting(E_ALL & ~E_NOTICE); + +如果在错误被触发后得到的是一个空白的结果,你的服务器可能关闭了错误提示。你可以在 `error_reporting` 调用前使用下面的代码开启错误提醒: + + ini_set('display_errors', TRUE); + +在发送错误提示时,错误应该**实时**显示,甚至是在上线发布之后,因为它允许你使用[异常和错误句柄](debugging.errors) 指引到一个友好的错误页面从而代替空白的页面。 diff --git a/includes/kohana/modules/userguide/guide/zh-cn/about.install.md b/includes/kohana/modules/userguide/guide/zh-cn/about.install.md new file mode 100644 index 00000000..a7c10b30 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/about.install.md @@ -0,0 +1,95 @@ +# 系统安装 + +1. 从 [Kohana 官方网站](http://kohanaframework.org/)下载最新**稳定**版本的框架 +2. 创建一个名为 'kohana' 的目录并解压缩到这个目录 +3. 上传到这个目录的所有文件到你的服务器上 +4. 编辑 `application/bootstrap.php` 文件并按实际情况修改下面配置: + - 为你的程序设置默认[时区](http://php.net/timezones) + - 在 [Kohana::init] 方法中设置 `base_url` 的值为 kohana 目录的路径(或域名地址) +6. 确保 `application/cache` 目录和 `application/logs` 目录让服务器可写权限 +7. 在你喜欢的浏览器地址栏中输入 `base_url` 来测试 Kohana 是否安装成功 + +[!!] 根据系统平台的不同,安装的目录可能会随着解压缩而失去原先的权限属性。如果有错误发生请在 Kohana 根目录设定所有文件属性为 755。命令为:`find . -type d -exec chmod 0755 {} \;` + +如果你可以看到安装页面(install.php)则说明已经安装成功(一片绿色),如果它报告有任何的错误(红色显示),你应该在立刻修复。 + +![安装页面](img/install.png "Example of install page") + +一旦安装页面报告你的环境确认无误,并且可以改名或删除在跟目录的 `install.php` 文件,然后你就能看到 Kohana 的欢迎界面: + +![欢迎界面](img/welcome.png "Example of welcome page") + + +## 设置产品(Production)环境 + +在转移到产品环境之前有些事情需要完成: + +1. 查看文档的[配置页面](about.configuration)。 + 它涵盖了大多数的环境全局设置。 + 一般来讲,在产品环境下需要开启缓存并关闭概况分析(profiling)([Kohana::init] 设置)。 + 如果设置了很多路由,路由缓存也是很有必要的。 +2. 在 application/bootstrap.php 捕获所有的异常,已保证敏感信息不会被堆栈跟踪泄漏。 + 下面有一个从 Shadowhand 的 wingsc.com 网站源代码提取出来的样例。 +3. 打开 APC 或某些类型的指令缓存。 + 这是最简单容易的提升 PHP 自身性能的方法。程序越复杂,使用指令缓存带来越大的利益。 + + /** + * Set the environment string by the domain (defaults to 'development'). + */ + Kohana::$environment = ($_SERVER['SERVER_NAME'] !== 'localhost') ? Kohana::PRODUCTION : Kohana::DEVELOPMENT; + /** + * Initialise Kohana based on environment + */ + Kohana::init(array( + 'base_url' => '/', + 'index_file' => FALSE, + 'profile' => Kohana::$environment !== Kohana::PRODUCTION, + 'caching' => Kohana::$environment === Kohana::PRODUCTION, + )); + + /** + * Execute the main request using PATH_INFO. If no URI source is specified, + * the URI will be automatically detected. + */ + $request = Request::instance($_SERVER['PATH_INFO']); + + try + { + // Attempt to execute the response + $request->execute(); + } + catch (Exception $e) + { + if ( Kohana::$environment == 'development' ) + { + // Just re-throw the exception + throw $e; + } + + // Log the error + Kohana::$log->add(Kohana::ERROR, Kohana::exception_text($e)); + + // Create a 404 response + $request->status = 404; + $request->response = View::factory('template') + ->set('title', '404') + ->set('content', View::factory('errors/404')); + } + + if ($request->send_headers()->response) + { + // Get the total memory and execution time + $total = array( + '{memory_usage}' => number_format((memory_get_peak_usage() - KOHANA_START_MEMORY) / 1024, 2).'KB', + '{execution_time}' => number_format(microtime(TRUE) - KOHANA_START_TIME, 5).' seconds'); + + // Insert the totals into the response + $request->response = str_replace(array_keys($total), $total, $request->response); + } + + + /** + * Display the request response. + */ + echo $request->response; + diff --git a/includes/kohana/modules/userguide/guide/zh-cn/about.kohana.md b/includes/kohana/modules/userguide/guide/zh-cn/about.kohana.md new file mode 100644 index 00000000..dc60b6d5 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/about.kohana.md @@ -0,0 +1,15 @@ +# 什么是 Kohana? + +Kohana 是由志愿者团队开发的一个开源形式使用 [PHP5](http://php.net/manual/intro-whatis "PHP Hypertext Preprocessor") 开发的[面对对象](http://wikipedia.org/wiki/Object-Oriented_Programming)模式 [MVC](http://wikipedia.org/wiki/Model–View–Controller "Model View Controller") 架构的 [Web 框架](http://wikipedia.org/wiki/Web_Framework)。它目标旨在快速开发,高安全性,轻量级代码。 + +[!!] Kohana 是基于 [BSD license](http://kohanaframework.org/license) 发布,所以大家可以用它开发任何的开源形式的,公司形式的或者个人形式的应用。 + +## Kohana 的特点是什么? + +使用独有的 [文件系统](about.filesystem) 设计可以任意继承库类而经可能少的(或不需要)[配置](about.configuration),[错误句柄](debugging.errors)能够在开发过程中迅速定位错误源,并有内置的[调试器](debugging.overview)和[分析器](debugging.profiling)作为辅助开发工具。 + +并且提供为您的应用程序提供安全系数相关的工具:[XSS removal](security.xss),[input validation](security.validation),[signed cookies](security.cookies),[form](security.forms) 和 [HTML](security.html)。于此同时,[数据库](security.database)层提供对 [SQL 注入](http://wikipedia.org/wiki/SQL_Injection)的保护。这是因为所有的官方代码经过精心编写和安全审查的。 + +## 这文档太逊了! + +我们正在全力提供完整的文档。如果你想从问题中找到答案,请查阅[非官方 Wiki](http://kerkness.ca/wiki/doku.php)。如果你想要添加或修改文档的内容,请 [fork](http://github.com/kohana/userguide) 本文档保存后发送 pull 请求。如果你不善于使用 git,你同样可以提交[feature request](http://dev.kohanaframework.org/projects/kohana3/issues)(需要注册)。 diff --git a/includes/kohana/modules/userguide/guide/zh-cn/about.mvc.md b/includes/kohana/modules/userguide/guide/zh-cn/about.mvc.md new file mode 100644 index 00000000..035b6b19 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/about.mvc.md @@ -0,0 +1,7 @@ +# (层次结构)模型 视图 控制器 + +模型 视图 控制器,英文全称为 Model View Controller(缩写 MVC)是一个流行的设计模式,它从呈现/模板(视图)和请求流程(控制器)中分离了数据源(模型)。 + +使用此模式设计开发系统级别的应用程序会更加容易和最大限度的重用代码,这就意味着你不必在写那么多不必要的代码了! + +[!!] Stub diff --git a/includes/kohana/modules/userguide/guide/zh-cn/about.upgrading.md b/includes/kohana/modules/userguide/guide/zh-cn/about.upgrading.md new file mode 100644 index 00000000..30bc5713 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/about.upgrading.md @@ -0,0 +1,288 @@ +# 从 2.3.x 升级 + +Kohana v3 大部分功能都不同于 Kohana 2.3 版本,下面列出了一系列的升级建议: + +## 命名约定 + +在 2.x 体系中不同的类的'类型'(比如 controller,model 等)使用后缀来加以区分。文件夹在模型/控制器目录下没有任何类名的关系。 + +在 3.0 版本中废弃了上面的形式转而使用 Zend framework 的文件体系的约定,也就是类名包含类名和其路径,之间是有下划线分割而不是斜杠符(比如 `/some/class/file.php` 变为了 `Some_Class_File`) +详情请参见 [约定文档](start.conventions) + +## Input 库 + +Input 库已经从 3.0 版本中移除,请使用 `$_GET` 和 `$_POST` 获取。 + +### XSS 保护 + +假如你需要使用 XSS 清除用户输入数据,你可以使用 [Security::xss_clean] 处理输入数据,比如: + + $_POST['description'] = security::xss_clean($_POST['description']); + +你也可以把 [Security::xss_clean] 当作 [Validate] 类的过滤器使用: + + $validation = new Validate($_POST); + + $validate->filter('description', 'Security::xss_clean'); + +### POST & GET + +Input 库有一个最大的方便之处在于如果你试图从一个超全域阵列(superglobal arrays)访问它的值,假若其值不存在 Input 库则会返回一个指定的默认值,比如: + + $_GET = array(); + + // $id 获得的值是 1 + $id = Input::instance()->get('id', 1); + + $_GET['id'] = 25; + + // $id 现在获得的值是 25 + $id = Input::instance()->get('id', 1); + +在 3.0 版本你可以使用 [Arr::get] 方法实现同样的效果: + + $_GET = array(); + + // $id 获得的值是 1 + $id = Arr::get($_GET, 'id', 1); + + $_GET['id'] = 42; + + // $id 现在获得的值是 42 + $id = Arr::get($_GET, 'id', 1); + +## ORM 库 + +自 2.3 版本到现在已经有一些主要的改动,下面是常见的通用升级问题: + +### 成员变量 + +现在所有的成员变量都添加了 下划线(_) 作为前缀而且无法再通过 `__get()` 方法获得访问权利。相反的你可以把属性名并去掉下划线当作函数去调用。 + +例如,在 2.3 版本中有一个 `loaded` 属性,现在改名为 `_loaded` 并且如果需要在外边类库中访问此属性只需要 `$model->loaded()`。 + +### 关系 + +在 2.3 版本中如果你想要迭代一个模型相关对象的话,你需要: + + foreach($model->{relation_name} as $relation) + +然而,在新的系统中这已经失效。在 2.3 版本中任何使用 Databate 库生成的查询都是在全局作用域生成,这就意味着你不能同时尝试和构建两个查询语句。这里有个例子: + +# TODO: 需要一个具体的实例!!!! + +第二此查询则会失效而不能查询,内部查询将 '继承' 作为第一条件,从而造成混乱。在 3.0 版本中此问题得到了有效的解决,创建每条查询都是其自身的作用域之中,尽管如此,这也意味着有些东西没法按实际的预期正常工作。这里有个例子: + + foreach(ORM::factory('user', 3)->where('post_date', '>', time() - (3600 * 24))->posts as $post) + { + echo $post->title; + } + +[!!] (相关新的查询语法请查看 [Database 教程](tutorials.databases)) + +在 2.3 版本中你希望它可以返回用户为 3 且 `post_date` 在最近 24 小时内发布的所有 posts 的迭代器,然而相反的,它将适用 where 语句到 user 模型中并返回带有指定加入语句的 'Model_Post' 对象。 + +为了达到 2.3 版本的同样效果,你只需要略微修改结构即可: + + foreach(ORM::factory('user', 3)->posts->where('post_date', '>', time() - (36000 * 24))->find_all() as $post) + { + echo $post->title; + } + +这同样也应用到 `has_one` 关系中: + + // 错误 + $user = ORM::factory('post', 42)->author; + // 正确 + $user = ORM::factory('post', 42)->author->find(); + +### Has and belongs to many relationships + +在 2.3 版本中你可以设置 `has_and_belongs_to_many` 关系。但是在 3.0 版本此功能已经融合到了 `has_many` *through*。 + +在你的模型中定义一个 `has_many` 关系到其他模型中,并且添加一个 `'through' => 'table'` 属性,其中 `'table'` 是连接表的名称。比如(posts<>categories): + + $_has_many = array + ( + 'categories' => array + ( + 'model' => 'category', // 外部模型 + 'through' => 'post_categories' // 连接表 + ), + ); + +如果你的数据库配置设置了表前缀,这也不用担心去添加表前缀。 + +### 外键 + +如果你想在 2.x 版本的 ORM 中覆写一个外键,你必须指定关系属于谁,并且你的新外键在成员变量 `$foreign_keys` 之中。 + +在 3.0 版本中你只需要在关系数组中定义一个 `foreign_key` 键即可,比如: + + Class Model_Post extends ORM + { + $_belongs_to = array + ( + 'author' => array + ( + 'model' => 'user', + 'foreign_key' => 'user_id', + ), + ); + } + +在上面的实例中我们应该在 posts 表中存在一个 `user_id` 字段。 + + + +In has_many relationships the `far_key` is the field in the through table which links it to the foreign table & the foreign key is the field in the through table which links "this" model's table to the through table. + +考虑以下设定,"Posts" have and belong to many "Categories" through `posts_sections`. + +| categories | posts_sections | posts | +|------------|------------------|---------| +| id | section_id | id | +| name | post_id | title | +| | | content | + + Class Model_Post extends ORM + { + protected $_has_many = array( + 'sections' => array( + 'model' => 'category', + 'through' => 'posts_sections', + 'far_key' => 'section_id', + ), + ); + } + + Class Model_Category extends ORM + { + protected $_has_many = array ( + 'posts' => array( + 'model' => 'post', + 'through' => 'posts_sections', + 'foreign_key' => 'section_id', + ), + ); + } + + +显然,这里的别名设定是有点疯狂,但它是如何让 foreign/far 键很好工作的绝佳范例。 + +### ORM 迭代器 + +`ORM_Iterator` 也是值得注意的改动,它已经融合到了 Database_Result 之中。 + +如果你想要获得带有对象主键的 ORM 对象数组,你只需要调用 [Database_Result::as_array],比如: + + $objects = ORM::factory('user')->find_all()->as_array('id'); + +其中的 `id` 就是 user 表的主键。 + +## Router 库 + +在 2.x 版本中有一个 Router 库用于处理主要的请求工作。它允许你在 `config/routes.php` 配置文件中定义基本的路由,而且它还允许支持自定义的正则表达式路由,尽管如此,如果你想做极端的话它就显得相当呆板。 + +## 路由 + +在 3.0 版本中路由系统(现在成为请求系统)有了更多的灵活度。路由现在全部定义在 bootstrap 文件中(`application/bootstrap.php`)以及模块(Module)的 init.php 文件之中(`modules/module/init.php`)。(另外值得一提的是,现在的路由是按照他们定义的顺序评估) + +替换定义的路由数组,你现在为每个路由创建一个新的 [Route] 对象。不像在 2.x 体系一样没有必要映射一个 uri 到另一个。相反的你使用标记段(比如,controller,method,id)的变量来指定 uri 模式。 + +例如,在老系统的正则: + + $config['([a-z]+)/?(\d+)/?([a-z]*)'] = '$1/$3/$1'; + +需要映射 uri 的 `controller/id/method` 为 `controller/method/id`,在 3.0 版本中这样修改: + + Route::set('reversed','((/(/)))') + ->defaults(array('controller' => 'posts', 'action' => 'index')); + +[!!] 每个 uri 都必须指定一个独一无二的名称(这里定义的是 `reversed`),其背后的原因是解释在 [URL 教程](tutorials.urls) 之中。 + +尖括号的内容会当作动态解析部分。圆括号的内容则会当作是可选或不必要的字段。如果你只是想匹配 uris 的开头是 admin,你只需要: + + Rouse::set('admin', 'admin(/(/(/)))'); + +但,如果你想用户必须指定一个控制器: + + Route::set('admin', 'admin/(/(/))'); + +同样,Kohana 不使用任何的 '默认的默认项'。如果你想让 Kohana 去设置默认 action 为 'index',你只需要使用 [Route::defaults] 设置即可!如果你需要为 uri 字段自定义正则表达式,你只需要以 `segment => regex` 传递数组,比如: + + Route::set('reversed', '((/(/)))', array('id' => '[a-z_]+')) + ->defaults(array('controller' => 'posts', 'action' => 'index')) + +这会迫使 id 的值必须全部是小写字母或者是数字,下划线。 + +### Actions + +还有一点我们必须要提到的,如果控制器中的方法可以通过网址访问,现在被称为 "actions",且其前缀为 'action_'。比如,在上面的例中,如果用户访问 `admin/posts/1/edit`,那么 "actions" 就是 'edit' 而且方法在控制器将会是 `action_edit`。详情请参见 [URL 教程](tutorials.urls) + +## Sessions + +以下方法不再存在:Session::set_flash(),Session::keep_flash() 和 Session::expire_flash() 方法,替代这些废弃方法的函数你可以使用 [Session::get_once]。 + +## URL 辅助函数 + +URL 辅助函数仅做了略微的改动 - `url::redirect()` 方法转移到了 `$this->request->redirect()` 之中(包含控制器)/ `Request::instance()->redirect()` + +`url::current` 现在替换为了 `$this->request->uri()` + +## Valid / Validation + +这恋歌类现在已经合并为一个类并命名为 `Validate`. + +对于校验数组的语法也有些改动: + + $validate = new Validate($_POST); + + // 应用一个过滤器到所有数组项中 + $validate->filter(TRUE, 'trim'); + + // 定义规则使用 rule() 方法 + $validate + ->rule('field', 'not_empty') + ->rule('field', 'matches', array('another_field')); + + // 为单字段设置多个规则也使用 rules() 方法,以 rules => params 的数组方式作为第二参数 + $validate->rules('field', array( + 'not_empty' => NULL, + 'matches' => array('another_field') + )); + +为保证定义明确,其中 'required' 规则现已经改名为 'not_empty'。 + +## View 库 + +对于 View 库也有一些值得注意的主要改动。 + +在 2.3 版本中视图在其处理的控制器中调用呈现,并允许你使用 `$this` 作为视图应用引用到控制器中。这一点在 3.0 版本改变了。视图现在呈现在一个空白的作用域,如果你需要在视图中使用 `$this`,你可以使用 [View::bind] 绑定一个引用 - `$view->bind('this', $this)` + +It's worth noting, though, that this is *very* bad practice as it couples your view to the controller, preventing reuse. 推荐的方法是像下面这样去传递必备的变量到视图中: + + $view = View::factory('my/view'); + + $view->variable = $this->property; + + // 或者如果你想使用连接方式 + + $view + ->set('variable', $this->property) + ->set('another_variable', 42); + + // 不推荐 + $view->bind('this', $this); + +因为视图在一个空的作用域呈现,而 `Controller::_kohana_load_view` 现在是多余的了。如果你想在它呈现之前修改视图(比如,添加一个站点的菜单),你可以使用 [Controller::after] + + Class Controller_Hello extends Controller_Template + { + function after() + { + $this->template->menu = '...'; + + return parent::after(); + } + } diff --git a/includes/kohana/modules/userguide/guide/zh-cn/debugging.code.md b/includes/kohana/modules/userguide/guide/zh-cn/debugging.code.md new file mode 100644 index 00000000..b6105225 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/debugging.code.md @@ -0,0 +1,18 @@ +# 调试 + +Kohana 默认加载一些功能强大的功能来辅助调试程序。 + +最常使用也是最基本的 [Kohana::debug] 方法。这个简单的方法会显示任何的变量数字,类似于 [var_export](http://php.net/var_export) 或 [print_r](http://php.net/print_r),但是使用了 HTML 格式输出。 + + // 显示 $foo 和 $bar 变量的相关信息 + echo Kohana::debug($foo, $bar); + +Kohana 也提供一个方法 [Kohana::debug_source] 来显示特定文件的源代码。 + + // 显示当前行的源代码 + echo Kohana::debug_source(__FILE__, __LINE__); + +如果你希望显示你应用文件的信息而不泄漏安装路径,你可以使用[Kohana::debug_path]: + + // 显示 "APPPATH/cache" 而不是真实路径 + echo Kohana::debug_path(APPPATH.'cache'); diff --git a/includes/kohana/modules/userguide/guide/zh-cn/debugging.errors.md b/includes/kohana/modules/userguide/guide/zh-cn/debugging.errors.md new file mode 100644 index 00000000..0cf6d963 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/debugging.errors.md @@ -0,0 +1,22 @@ +# 错误/异常句柄 + +Kohana 同时提供了异常句柄和错误句柄使用 PHP 的 [ErrorException](http://php.net/errorexception) 类转换错误为异常。关于错误的详情和通过句柄显示应用程序的内部状态: + +1. Exception 类 +2. 错误等级 +3. 错误信息 +4. 带有行高亮的错误源 +5. 执行流程的[调试跟踪](http://php.net/debug_backtrace) +6. 导入(include)文件,加载扩展,全局变量 + +## 实例 + +点击任何一个链接可以切换显示额外的信息: + +
          {{userguide/examples/error}}
          + +## 显示错误/异常句柄 + +如果您不希望使用内部错误句柄,您可以调用 [Kohana::init] 时禁用它: + + Kohana::init(array('errors' => FALSE)); diff --git a/includes/kohana/modules/userguide/guide/zh-cn/debugging.profiling.md b/includes/kohana/modules/userguide/guide/zh-cn/debugging.profiling.md new file mode 100644 index 00000000..95ebe61a --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/debugging.profiling.md @@ -0,0 +1,20 @@ +# 分析器 + +Kohana 提供一种非常简单的方法来显示你的程序的统计信息 + +1. 普通 [Kohana] 方法调用 +2. 请求 +3. [Database] 查询 +4. 程序的平均运行时间 + +## 实例 + +你可以在任何时候显示或收集当前 [profiler] 统计: + +
          + +
          + +## 预览 + +{{profiler/stats}} \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/zh-cn/features.md b/includes/kohana/modules/userguide/guide/zh-cn/features.md new file mode 100644 index 00000000..4f7e05e5 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/features.md @@ -0,0 +1 @@ +本页面将会列举 Kohana v3 的特性。 \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/zh-cn/menu.md b/includes/kohana/modules/userguide/guide/zh-cn/menu.md new file mode 100644 index 00000000..7de47433 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/menu.md @@ -0,0 +1,31 @@ +1. **入门指南** + - [什么是 Kohana?](about.kohana) + - [约定和样式](about.conventions) + - [模型-视图-控制器](about.mvc) + - [级联文件系统](about.filesystem) + - [请求流程](about.flow) + - [安装](about.install) + - [升级](about.upgrading) + - [API 浏览器](api) +3. **基本使用** + - [配置](using.configuration) + - [类的加载](using.autoloading) + - [视图和 HTML](using.views) + - [Sessions 和 Cookies](using.sessions) + - [消息](using.messages) +6. **调试** + - [代码](debugging.code) + - [错误句柄](debugging.errors) + - [分析器](debugging.profiling) +5. **安全** + - [XSS](security.xss) + - [校验](security.validation) + - [Cookies](security.cookies) + - [数据库](security.database) +4. **教程** + - [Hello, World](tutorials.helloworld) + - [路由,URLs 和链接](tutorials.urls) + - [清理 URLs](tutorials.removeindex) + - [数据库](tutorials.databases) + - [ORM](tutorials.orm) + - [使用 Git 开发](tutorials.git) diff --git a/includes/kohana/modules/userguide/guide/zh-cn/security.validation.md b/includes/kohana/modules/userguide/guide/zh-cn/security.validation.md new file mode 100644 index 00000000..796e956d --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/security.validation.md @@ -0,0 +1,244 @@ +# 效验 + +使用 [Validate] 类可以对任意的数组进行校验。标签,过滤器,规则和回调函数都以数组的键(称之为 "字段名")附属于 Validate 对象。 + +标签(labels) +: 标签是人们可读取的字段名。 + +过滤器(filters) +: 过滤器必须在规则和回调函数之前调用执行做预处理。 + +规则(rules) +: 规则是用于检测字段并返回结果 `TRUE` 或 `FALSE`。 + 如果返回 `FALSE`,其对于的错误会添加到字段中。 + +回调函数(callbacks) +: 回调函数是自定义函数,它可以访问整个校验对象。 + 回调函数的返回值会被忽略,因此,当校验错误时回调函数必须手动的使用 [Validate::error] 添加错误到对象中。 + +[!!] 注意 [Validate] 的 callbacks 和 [PHP callbacks](http://php.net/manual/language.pseudo-types.php#language.types.callback) 是完全不同的两个方法。 + +如果想把添加的过滤器,规则或回调函数应用到所有的定义的字段需要设置字段名为 `TRUE` 。 + +**[Validate] 对象会移除所有未设置标签,过滤器,规则或回调函数的字段。以此防止未被验证的字段发生校验错误。** + +使用 [Validate::factory] 方法创建校验对象: + + $post = Validate::factory($_POST); + +[!!] 提示 `$post` 对象将会被用于本教程的其他实例中。 + +### 默认规则 + +校验默认提供的规则: + +规则名称 | 函数 +------------------------- |------------------------------------------------- +[Validate::not_empty] | 值不能为空值 +[Validate::regex] | 值使用正则表达式匹配 +[Validate::min_length] | 值的最小长度 +[Validate::max_length] | 值的最大长度 +[Validate::exact_length] | 值的长度必须是这里指定的长度 +[Validate::email] | 值必须是 Email 地址 +[Validate::email_domain] | 检查 Email 的域是否存在 +[Validate::url] | 值必须是 URL +[Validate::ip] | 值必须是 IP 地址 +[Validate::phone] | 值必须是电话号码 +[Validate::credit_card] | 值必须是信用卡号 +[Validate::date] | 值必须是日期(时间) +[Validate::alpha] | 仅允许英文字母 +[Validate::alpha_dash] | 仅允许英文字母和连词符号(-) +[Validate::alpha_numeric] | 仅允许英文字母和数字 +[Validate::digit] | 仅允许整数 +[Validate::decimal] | 值必须是小数或浮点数 +[Validate::numeric] | 仅允许数字 +[Validate::range] | 值必须是某一范围内的值 +[Validate::color] | 值必须是有效的 HEX 颜色 +[Validate::matches] | 值必须匹配其他字段的值 + +[!!] 任何存在于 [Validate] 类中的方法都可以在不指定完整回调的情况下用于校验规则。 +比如,添加 `'not_empty'` 和 `array('Validate', 'not_empty')` 是等同的。 + +## 添加过滤器 + +所有的校验规则被定义为字段名,方法或函数(使用 [PHP callback](http://php.net/callback) 语法)以及数组形式的参数: + + $object->filter($field, $callback, $parameter); + +过滤器修改字段值之前请仔细检查规则或回调函数。 + +如果要转换 "username" 字段的值为全小写: + + $post->filter('username', 'strtolower'); + +如果要对所有字段移除左右*所有*空格: + + $post->filter(TRUE, 'trim'); + +## 添加规则 + +所有的校验规则被定义为字段名,方法或函数(使用 [PHP callback](http://php.net/callback) 语法)以及数组形式的参数: + + $object->rule($field, $callback, $parameter); + +### 实例 + +任何函数添加到 `Validate` 类都可以通过调用一个规则而不必指定 `Validate` 类: + + $post + ->rule('username', 'not_empty') + ->rule('username', 'regex', array('/^[a-z_.]++$/iD')) + + ->rule('password', 'not_empty') + ->rule('password', 'min_length', array('6')) + ->rule('confirm', 'matches', array('password')) + + ->rule('use_ssl', 'not_empty'); + +任何 PHP 可用的函数也可以当作规则。比如,如果我们要检测用户是否使用 SSL: + + $post->rule('use_ssl', 'in_array', array(array('yes', 'no'))); + +[!!] 注意:所有的参数类数组都必须在一个数组内! + +所有其他自定义的规则也可以作为回调函数添加进来: + + $post->rule('username', array($model, 'unique_username')); + +回调方法 `$model->unique_username()` 的代码如下: + + public function unique_username($username) + { + // 检测用户名是否存在于数据库 + return ! DB::select(array(DB::expr('COUNT(username)'), 'total')) + ->from('users') + ->where('username', '=', $username) + ->execute() + ->get('total'); + } + +[!!] 自定义规则可以设置许多额外的检测以可用于多种用途。这些方法运行存在于一个模型(model)中,或者是定义在任意一个类中。 + +## 添加回调函数 + +所有的校验规则被定义为字段名,方法或函数(使用 [PHP callback](http://php.net/callback) 语法)以及数组形式的参数: + + $object->callback($field, $callback); + +[!!] 不同的过滤器和规则,没有参数也可以传递到回调函数之中。 + +如果用户的密码必须是哈希值,我们可以使用回调函数哈希其值: + + $post->callback('password', array($model, 'hash_password')); + +假设 `$model->hash_password()` 方法是类似这样定义的: + + public function hash_password(Validate $array, $field) + { + if ($array[$field]) + { + // 如果存在此字段进行哈希操作 + $array[$field] = sha1($array[$field]); + } + } + +# 一个完整的例子 + +首先,我们使用 [View] 创建一个 HTML 表单。假设文件存放于 `application/views/user/register.php`: + + + +

          操作发生问题,请仔细检查并保证填写正确。

          +
            + +
          • + + + +
            +
            +
            + +
            +
            +
            密码必须保证至少六位字符
            +
            +
            + +
            +
            '总是使用 SSL', 'no' => '仅当需要的时候'), $post['use_ssl']) ?>
            +
            鉴于安全起见,SSL 一般用于支付时使用
            +
            + + + + +[!!] 本例子我们使用了 [Form] 辅助函数生成表单。使用 [Form] 从而代替手写 HTML 代码的好处在于所有输入项都会严格处理。 +如果你喜欢手写 HTML,那请记得使用 [HTML::chars] 方法来转移用户输入。 + +接下来,我们开始编写控制器的代码来处理注册过程。假设文件存放于 `application/classes/controller/user.php`: + + class Controller_User extends Controller { + + public function action_register() + { + $user = Model::factory('user'); + + $post = Validate::factory($_POST) + ->filter(TRUE, 'trim') + + ->filter('username', 'strtolower') + + ->rule('username', 'not_empty') + ->rule('username', 'regex', array('/^[a-z_.]++$/iD')) + ->rule('username', array($user, 'unique_username')) + + ->rule('password', 'not_empty') + ->rule('password', 'min_length', array('6')) + ->rule('confirm', 'matches', array('password')) + + ->rule('use_ssl', 'not_empty') + ->rule('use_ssl', 'in_array', array(array('yes', 'no'))) + + ->callback('password', array($user, 'hash_password')); + + if ($post->check()) + { + // 确保数据都通过了校验后执行注册用户 + $user->register($post); + + // 通常在注册成功后会调整到登录前的页面 + URL::redirect('user/profile'); + } + + // 校验失败,获得错误提示 + $errors = $post->errors('user'); + + // 显示用户注册的表单 + $this->request->response = View::factory('user/register') + ->bind('post', $post) + ->bind('errors', $errors); + } + + } + +另外我们还需要有一个 user 模型,假设文件存放于 `application/classes/model/user.php`: + + class Model_User extends Model { + + public function register($array) + { + // 创建一条新纪录 + $id = DB::insert(array_keys($array)) + ->values($array) + ->execute(); + + // 保存新用户的 id 到 cookie + cookie::set('user', $id); + + return $id; + } + + } + +一个简单的用户注册的例子就这么完成了! diff --git a/includes/kohana/modules/userguide/guide/zh-cn/security.xss.md b/includes/kohana/modules/userguide/guide/zh-cn/security.xss.md new file mode 100644 index 00000000..1d7da562 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/security.xss.md @@ -0,0 +1,15 @@ +# 跨站脚本(XSS)安全 + +首先大家先要了解什么是 [XSS](http://wikipedia.org/wiki/Cross-Site_Scripting) 之后才能更好的保护自己。XSS 只能在 HTML 代码中才能触发,可能通过表单的输入或者从数据库结果显示。任何全局变量包括客户信息都可能被感染。这包括 `$_GET`,`$_POST` 和 `$_COOKIE` 中的数据。 + +## 预防措施 + +这里有一些简单的方法可以预防你的程序不受 XSS 的侵害。第一个方法是使用 [Security::xss] 方法处理所有全局变量的输入数据。如果你不想让变量里有 HTML 代码,你可以使用 [strip_tags](http://php.net/strip_tags) 从值中移除所有的 HTML 标签。 + +[!!] 如果用户提交 HTML 到你的程序之中,最好的推荐方法是使用类似 [HTML Purifier](http://htmlpurifier.org/) 或 [HTML Tidy](http://php.net/tidy) 这样的 HTML 代码清理工具。 + +第二个方法是转义输入的 HTML 代码。[HTML] 类提供生成大多数的标签,其中包括脚本(script)和样式表(stylesheet)链接,超级链接,图片,邮箱(emailto)链接。任何不可信的内容都会使用 [HTML::chars] 去转义。 + +## 参考资料 + +* [OWASP XSS Cheat Sheet](http://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet) diff --git a/includes/kohana/modules/userguide/guide/zh-cn/tutorials.databases.md b/includes/kohana/modules/userguide/guide/zh-cn/tutorials.databases.md new file mode 100644 index 00000000..4f99eaa3 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/tutorials.databases.md @@ -0,0 +1,248 @@ +# 数据库 {#top} + +Kohana 3.0 采用了更加健壮的模块方式开发。默认情况下数据库模块支持 [MySQL](http://php.net/mysql) 和 [PDO](http://php.net/pdo) 两种驱动方式。 + +默认 Kohana 3.0 中包含了数据库模块,但是使用之前必须在 `application/bootstrap.php` 文件中的 [Kohana::modules] 方法中开启它。 + + Kohana::modules(array( + ... + 'database' => MODPATH.'database', + ... + )); + +## 配置 {#configuration} + +开启模块后接着需要配置数据库,这样才能保证数据库的正常连接。比如说配置文件存放在 `modules/database/config/database.php` 文件中。 + +数据库配置组的结构,我们称之为 "instance",就像下面这个样子: + + string INSTANCE_NAME => array( + 'type' => string DATABASE_TYPE, + 'connection' => array CONNECTION_ARRAY, + 'table_prefix' => string TABLE_PREFIX, + 'charset' => string CHARACTER_SET, + 'profiling' => boolean QUERY_PROFILING, + ), + +[!!] 配置文件里可以定义多个 instances 配置组。 + +重点了解一下各项设置的含义。 + +INSTANCE_NAME +: 配置组的名称,任意命名,但是最好保留一个名为 "default" 的默认连接。 + +DATABASE_TYPE +: 数据库驱动类型。Kohana 目前支持 "mysql" 和 "pdo" 两种驱动。 + +CONNECTION_ARRAY +: 配置上述驱动的连接项。(驱动连接项在[下面](#connection_settings)有说明) + +TABLE_PREFIX +: 表前缀,用于通过 [查询器](#query_building) 添加到所有的表名。 + +QUERY_PROFILING +: 开始数据库查询的 [profiling](debugging.profiling)。 + +### 范例 + +范例中共给出了两种 MySQL 连接:一个是本地,一个是远程。 + + return array + ( + 'default' => array + ( + 'type' => 'mysql', + 'connection' => array( + 'hostname' => 'localhost', + 'username' => 'dbuser', + 'password' => 'mypassword', + 'persistent' => FALSE, + 'database' => 'my_db_name', + ), + 'table_prefix' => '', + 'charset' => 'utf8', + 'profiling' => TRUE, + ), + 'remote' => array( + 'type' => 'mysql', + 'connection' => array( + 'hostname' => '55.55.55.55', + 'username' => 'remote_user', + 'password' => 'mypassword', + 'persistent' => FALSE, + 'database' => 'my_remote_db_name', + ), + 'table_prefix' => '', + 'charset' => 'utf8', + 'profiling' => TRUE, + ), + ); + +### 连接设置 {#connection_settings} + +每种数据库驱动的连接方式各有不同。 + +#### MySQL + +在 `connection` 数组中 MySQL 数据库可接受下面选项: + +类型 | 选项 | 描述 | 默认值 +----------|------------|----------------------------| ------------------------- +`string` | hostname | Hostname of the database | `localhost` +`integer` | port | Port number | `NULL` +`string` | socket | UNIX socket | `NULL` +`string` | username | Database username | `NULL` +`string` | password | Database password | `NULL` +`boolean` | persistent | Persistent connections | `FALSE` +`string` | database | Database name | `kohana` + +#### PDO + +在 `connection` 数组中 PDO 数据库可接受下面选项: + +类型 | 选项 | 描述 | 默认值 +----------|------------|----------------------------| ------------------------- +`string` | dsn | PDO data source identifier | `localhost` +`string` | username | Database username | `NULL` +`string` | password | Database password | `NULL` +`boolean` | persistent | Persistent connections | `FALSE` + +[!!] 如果你使用的是 PDO 而且并不确定如何去配置 `dsn` 选项,请查阅 [PDO::__construct](http://php.net/pdo.construct) 的相关资料。 + +## 连接并实例化 {#connections} + +每个配置组都可以当作数据库的实例化对象。每个实例化都是通过调用 [Database::instance] 方法来访问: + + $default = Database::instance(); + $remote = Database::instance('remote'); + +关闭数据库连接的最简单的方法(销毁对象): + + unset($default, Database::$instances['default']); + +如果你想关闭所有数据库的实例化只需: + + Database::$instances = array(); + +## 如何查询 {#making_queries} + +这里共有两种不同的方法进行查询。最简单的一次查询方式是通过 [DB::query] 使用 [Database_Query] 来创建查询。这些查询被称之为 "预处理语句",并且允许设置的查询参数自动的转义。而第二种查询方式是通过方法调用来组建查询。它们都是通过[查询器](#query_building) 完成。 + +[!!] 所有的查询都必须调用 `execute` 方法才能进行运行查询,可接受一个 [Database] 对象或者实例化名称。详情请参见 [Database_Query::execute]。 + +### 预处理语句 + +使用预处理语句可以让你手动编写 SQL 语句的同时还能自动转义查询的值以防止 [SQL 注入](http://wikipedia.org/wiki/SQL_Injection)。首先我们先来一个简单的查询: + + $query = DB::query(Database::SELECT, 'SELECT * FROM users WHERE username = :user'); + +[DB::query] factory 方法为我们创建了一个新的 [Database_Query] 类并允许串连(chaining)。查询语句包含一个 `:user` 参数,这个参数我们可以分配给它一个值: + + $query->param(':user', 'john'); + +[!!] 参数名称可以是任意的可以使用 [strtr](http://php.net/strtr) 替换的字符串。强烈建议**不要**使用美元符号当作参数名以免混淆。 + +如果你想显示 SQL 执行的语句,只需要简单的强制转换对象为字符串即可: + + echo Kohana::debug((string) $query); + // 应该显示: + // SELECT * FROM users WHERE username = 'john' + +如果你想更新 `:user` 参数只需要再次调用 [Database_Query::param] 即可: + + $query->param(':user', $_GET['search']); + +[!!] 如果你想一次设置多个参数,你需要使用 [Database_Query::parameters]。 + +当你分配完毕每个参数之后,你只需要执行下面的方法来执行查询语句: + + $query->execute(); + +使用[变量引用]((http://php.net/language.references.whatdo)) 也可以绑定参数到一个变量中。当多次执行同条语句的时候是非常管用的: + + $query = DB::query(Database::INSERT, 'INSERT INTO users (username, password) VALUES (:user, :pass)') + ->bind(':user', $username) + ->bind(':pass', $password); + + foreach ($new_users as $username => $password) + { + $query->execute(); + } + +在上面的例子中,变量 `$username` 和 `$password` 在每次使用 `foreach` 语句循环的时候都会改变。如果变量改变了,那么语句中的参数 `:user` 和 `:pass` 也会跟着改变。这种方法使用得当的话是非常节省时间的。 + +### 查询器 {#query_building} + +使用对象和方法动态查询使得查询语句可以以一种不可知论的方法迅速的组建起来。查询器也添加了和值引用一样好的标识符(表和列名)引用。 + +[!!] 目前为止,查询器无法有效的和预处理语句组合。 + +#### SELECT + +每种数据库查询类型都是用过不同的类引用,它们每一个都有自己的方法。比如,创建一个 SELECT 查询,我们需要使用 [DB::select]: + + $query = DB::select()->from('users')->where('username', '=', 'john'); + +默认情况下,[DB::select] 会选择所有的列(`SELECT * ...`),但是你也可以指定返回的某些列: + + $query = DB::select('username', 'password')->from('users')->where('username', '=', 'john'); + +现在让我们花一点时间看看方法是如何串连完成的。首先,我们使用 [DB::select] 方法创建了一个选择对象。接着,我们使用 `from` 方法选择表。最后我们使用 `where` 方法查询一个指定的记录。好了,执行之后我们看看执行了怎么样的一条 SQL 语句,还是老方法,强制转换对象为字符串: + + echo Kohana::debug((string) $query); + // 应该显示: + // SELECT `username`, `password` FROM `users` WHERE `username` = 'john' + +注意列名,表名是如何和值很好的转义啊?这就是使用查询器最重要的好处。 + +查询的时候它也支持 `AS` 方式的别名: + + $query = DB::select(array('username', 'u'), array('password', 'p'))->from('users'); + +生成的 SQL 语句: + + SELECT `username` AS `u`, `password` AS `p` FROM `users` + +#### INSERT + +向数据库插入一条记录,我们使用 [DB::insert] 方法创建一条 INSERT 语句: + + $query = DB::insert('users', array('username', 'password'))->values(array('fred', 'p@5sW0Rd')); + +生成的 SQL 语句: + + INSERT INTO `users` (`username`, `password`) VALUES ('fred', 'p@5sW0Rd') + +#### UPDATE + +更新已存在的记录,我们使用 [DB::update] 方法创建一条 UPDATE 语句: + + $query = DB::update('users')->set(array('username' => 'jane'))->where('username', '=', 'john'); + +生成的 SQL 语句: + + UPDATE `users` SET `username` = 'jane' WHERE `username` = 'john' + +#### DELETE + +删除已存在的记录,我们使用 [DB::delete] 方法创建一条 DELETE 语句: + + $query = DB::delete('users')->where('username', 'IN', array('john', 'jane')); + +生成的 SQL 语句: + + DELETE FROM `users` WHERE `username` IN ('john', 'jane') + +#### 数据库函数 {#database_functions} + +有些时候,你可以会碰到这样的一个情况:当你需要在查询时调用 `COUNT` 或者一些其他数据库自身函数。其实查询器可以通过两种方法支持这些函数。第一种是使用别名引用: + + $query = DB::select(array('COUNT("username")', 'total_users'))->from('users'); + +这看起来十分的相似于 `AS` 别名,但是注意列名是通过双引号括起来的。任何时候一个带有双引号的值出现在列名内部的时候,这部分在双引号内部的引用**仅仅**只能被转义。上面的查询方法生成的 SQL 语句: + + SELECT COUNT(`username`) AS `total_users` FROM `users` + +#### 复杂的表达式 + +别名引用可以解决大部分的问题。但是有时你可能因需要复杂的表达式陷入困境。由于这些原因,你可以使用数据库表达式 [DB::expr] 创建。数据库表达式可以作为直接输入而并不会执行转义。 diff --git a/includes/kohana/modules/userguide/guide/zh-cn/tutorials.git.md b/includes/kohana/modules/userguide/guide/zh-cn/tutorials.git.md new file mode 100644 index 00000000..65b752e3 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/tutorials.git.md @@ -0,0 +1,143 @@ +# 使用 Git 开发 + +Kohana 使用 [git](http://git-scm.com/) 作为版本控制并托管在 [github](http://github.com/kohana) 网站上面。本教程将会讲解如何让你使用 Git 并从 github 上部署一个简单的应用。 + +## 安装配置 Git + +### 安装 Git + +- OSX: [Git-OSX](http://code.google.com/p/git-osx-installer/) +- Windows: [Msygit](http://code.google.com/p/msysgit/) +- 或者从 [Git 官网](http://git-scm.com/) 下载并手动安装(步骤请参考 Git 官网) + +### 设置全局配置 + + git config --global user.name "你的名字" + git config --global user.email "你的邮箱" + +### 附加偏好设置 + +为了让 git 命令和版本库在命令行达到更好的视觉效果,你可以额外设置: + + git config --global color.diff auto + git config --global color.status auto + git config --global color.branch auto + +### 设置自动补全 + +[!!] 以下步骤仅用于 OSX 机器 + +根据下面步骤一步步执行,全部完成之后自动补全就可以在 git 环境下工作了 + + cd /tmp + git clone git://git.kernel.org/pub/scm/git/git.git + cd git + git checkout v`git --version | awk '{print $3}'` + cp contrib/completion/git-completion.bash ~/.git-completion.bash + cd ~ + rm -rf /tmp/git + echo -e "source ~/.git-completion.bash" >> .profile + +### 总是使用 LF 作为换行符 + +此设置主要用于 Kohana。如果你愿意捐赠代码至社区请遵循此约定。 + + git config --global core.autocrlf input + git config --global core.savecrlf true + +[!!] 关于换行符的更多信息请参见 [github](http://help.github.com/dealing-with-lineendings/) + +### 参考资料 + +- [Git Screencasts](http://www.gitcasts.com/) +- [Git Reference](http://gitref.org/) +- [Pro Git book](http://progit.org/book/) + +## 部署系统结构 + +[!!] 开始本教程前务必保证开发环境以及设置完毕,接下来我们要做一个可以通过 访问的新应用。 + +打开你的控制台(译者:Windows 平台为命令提示符,*nux 平台为终端),创建并切换到 `gitorial` 空目录下面(译者:此目录即为新应用的目录),执行 `git init`。这是为当前目录创建一个新的 git 空版本库。 + +下一步,我们为 `system` 目录要创建一个 [submodule](http://www.kernel.org/pub/software/scm/git/docs/git-submodule.html)(子模块)。访问 页面并复制克隆(Clone) URL: + +![Github Clone URL](http://img.skitch.com/20091019-rud5mmqbf776jwua6hx9nm1n.png) + +现在使用复制后的 URL 去创建 `system` 子模块: + + git submodule add git://github.com/kohana/core.git system + +[!!] 上面的链接是 Kohana 为下一个稳定版本准备的当前的开发版本。开发版本几乎是可以拿来做开发的,他拥有当前稳定版本同样的 API 和一些补丁修复。 + +现在,我们准备添加自己开发所需的子模块。比如你可能需要使用 [Database](http://github.com/kohana/database) 模块: + + git submodule add git://github.com/kohana/database.git modules/database + +添加子模块之后,我们必须让其初始化: + + git submodule init + +子模块我们已经添加完毕,接着我们去提交当前版本: + + git commit -m 'Added initial submodules' + +下一步,创建应用文件结构。下面的是最低要求: + + mkdir -p application/classes/{controller,model} + mkdir -p application/{config,views} + mkdir -m 0777 -p application/{cache,logs} + +如果你执行 `find application` 你应该会看到: + + application + application/cache + application/config + application/classes + application/classes/controller + application/classes/model + application/logs + application/views + +如果我们不想让 git 去追踪日志(log)或者缓存(Cache)文件,我们需要为每个目录添加一个 `.gitignore` 文件。它会忽略所有的非隐藏文件: + + echo '[^.]*' > application/{logs,cache}/.gitignore + +[!!] Git 会忽略空目录,所有我们添加 `.gitignore` 文件以保证 git 会追踪其目录,但是不会追踪目录下面的文件。 + +现在我们还缺 `index.php` 和 `bootstrap.php` 文件: + + wget http://github.com/kohana/kohana/raw/master/index.php + wget http://github.com/kohana/kohana/raw/master/application/bootstrap.php -O application/bootstrap.php + +再次提交: + + git add application + git commit -m 'Added initial directory structure' + +所有的工作都完成了!你现在可以使用 Git 作为版本控制开发 Kohana 应用了。 + +## 更新子模块 + +有时候你可能也需要更新你的子模块。更新所有子模块至最新的 `HEAD` 版本: + + git submodule foreach 'git checkout master && git pull origin master' + +更新单个子模块,比如 `system`: + + cd system + git checkout master + git pull origin master + cd .. + git add system + git commit -m 'Updated system to latest version' + +如果你要更新单个子模块到指定的版本库: + + cd modules/database + git pull origin master + git checkout fbfdea919028b951c23c3d99d2bc1f5bbeda0c0b + cd ../.. + git add database + git commit -m 'Updated database module' + +完成! \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/zh-cn/tutorials.helloworld.md b/includes/kohana/modules/userguide/guide/zh-cn/tutorials.helloworld.md new file mode 100644 index 00000000..95590144 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/tutorials.helloworld.md @@ -0,0 +1,106 @@ +# Hello, World + +差不多每个框架提供的教程实例都会包括 Hello,World 这样的例子,于是我们也遵循这样的传统! + +下面我们将要创建一个非常非常基础的 Hello,World,然后我们将它慢慢扩展符合 MVC 架构的样例。 + +## 基础框架 + +首先,我们先要创建一个控制器,让 Kohana 可用于处理请求。 + +在 application 目录下创建 `application/classes/controller/hello.php` 文件并敲入下面代码: + + template->message = 'hello, world!'; + } + } + +`extends Controller_Template` +: 现在我们继承了模板控制器(Template Controller),使用它可以更加方便在控制器中使用视图。 + +`public $template = 'site';` +: 模板控制器需要知道你想要使用什么模板文件。它会自动加载这个变量中定义的视图并返回一个视图对象。 + +`$this->template->message = 'hello, world!';` +: `$this->template` 是我们站点模板的视图对象引用。这里我们分配一个名为 "message" 的变量其值为 "hello, world!" 到视图中。 + +现在让我们尝试运行代码... + +
            {{userguide/examples/hello_world_error}}
            + +出于某种原因 Kohana 会抛出一个不稳定的而没有正常显示我们期望的信息。 + +如果我们仔细查看错误信息,我们可以发现 View 库无法找到我们设定的模板文件,这可能是我们还没有创建它 – *doh*!(译注:doh 表达当发现事情朝坏的、不随人意的方向发展或某人说了傻话、做了蠢事时的情绪低落) + +马上开始创建视图文件 `application/views/site.php`: + + + + We've got a message for you! + + + +

            +

            We just wanted to say it! :)

            + + + +再次刷新刚才的错误页面,怎么样看到正确的结果了吧: + +![hello, world! We just wanted to say it!](img/hello_world_2.png "hello, world! We just wanted to say it!") + +## 第三阶段 – 成果! + +在本教程中你已经学会如何创建和使用控制器,以及使用视图分离逻辑来显示视图。 + +这绝对是一个非常基本教程来介绍如何使用 Kohana 工作,且它根本就不会影响你的潜力使用它来开发应用。 diff --git a/includes/kohana/modules/userguide/guide/zh-cn/tutorials.orm.md b/includes/kohana/modules/userguide/guide/zh-cn/tutorials.orm.md new file mode 100644 index 00000000..2c418dcb --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/tutorials.orm.md @@ -0,0 +1,299 @@ +# ORM {#top} + +Kohana 3.0 包含一个强劲的 ORM 扩展,作用于记录模式,数据库内省来确定模型的列表。 + +ORM 扩展默认包含在 Kohana 3.0 之中,但是如要使用它则需要先开启它。修改 `application/bootstrap.php` 文件中的 [Kohana::modules] 方法并加载 ORM 扩展: + + Kohana::modules(array( + ... + 'orm' => MODPATH.'orm', + ... + )); + +## 配置 {#configuration} + +通过一些配置 ORM 才能工作。通过在模型类继承 ORM: + + class Model_User extends ORM + { + ... + } + +上面的例子中,模型会寻找默认数据库的 `users` 表。 + +### 模型配置属性 + +下面的属性是用于配置每个模型的: + +类型 | 属性 | 描述 | 默认值 +----------|---------------------|----------------------------------| ------------------------- +`string` | _table_name | 表名 | `singular model name` +`string` | _db | 数据库配置名 | `default` +`string` | _primary_key | 主键 | `id` +`string` | _primary_val | 主键值 | `name` +`bool` | _table_names_plural | 表名是否是复数形式 | `TRUE` +`array` | _sorting | 列名 => 排序方向的数组 | `primary key => ASC` +`string` | _foreign_key_suffix | 外键的后缀 | `_id` + +## 使用 ORM + +### 加载一条记录 + +通过调用 [ORM::factory] 或 [ORM::__construct] 方法创建模型的实例化: + + $user = ORM::factory('user'); + // 或者 + $user = new Model_User(); + +构造函数和 factory 方法也接受一个主键值来加载模型数据: + + // 加载 ID 为 5 的用户 + $user = ORM::factory('user', 5); + + // 检查用户是否加载成功 + if ($user->loaded()) { ... } + +你同样可以使用传递键-值型数组的数据对象去加载记录: + + // 加载 email 为 oe@example.com 的用 + $user = ORM::factory('user', array('email' => 'joe@example.com')); + +### 搜索记录 + +ORM 支持大多数的 [Database] 方法来强劲驱动搜索模型中的数据。ORM 类的 `_db_methods` 属性列出了所有支持调用的方法列表。记录的搜索可以通过 [ORM::find] 和 [ORM::find_all] 方法调用获得。 + + // 搜索活跃用户中名为 Bob 的第一条记录 + $user = ORM::factory('user') + ->where('active', '=', TRUE) + ->where('name', '=', 'Bob') + ->find(); + + // 搜索名为 Bob 的所有用户 + $users = ORM::factory('user') + ... + ->find_all(); + +当你使用 [ORM::find_all] 搜索一批记录模型,你可以使用迭代从数据库结果中获取每条记录模型: + + foreach ($users as $user) + { + ... + } + +ORM 一个强大的特性是 [ORM::as_array] 方法,它把返回的记录集转为为一个数组。如果使用了 [ORM::find_all] 所有的记录都会以数组的形式返回。 +对于选择列的时候是非常好用的: + + // 显示选择列的用户名 (使用 id 作为其值) + form::select('user', ORM::factory('user')->find_all()->as_array('id', 'username') ... + +### 记录数 + +使用 [ORM::count_all] 方法返回查询返回记录集的记录数。 + + // 用户的记录数 + $count = ORM::factory('user')->where('active', '=', TRUE)->count_all(); + +如果你想在特定子集的查询语句中统计所有用户的记录数,在调用 `count_all` 方法之前先调用 [ORM::reset] 方法并赋值 `FALSE`: + + $user = ORM::factory('user'); + + // 用户的总数 (reset FALSE prevents the query object from being cleared) + $count = $user->where('active', '=', TRUE)->reset(FALSE)->count_all(); + + // 仅返回前 10 条记录的记录数 + $users = $user->limit(10)->find_all(); + +### 取出模型属性 + +所有的模型属性都可以通过 PHP 的魔法方法 `__get` 和 `__set` 得到读写权。 + + $user = ORM::factory('user', 5); + + // 输出用户名 + echo $user->name; + + // 更改用户名 + $user->name = 'Bob'; + +假如保存的信息/属性并不存在于模型表中,使用 `_ignored_columns` 来忽略数据成员。 + + class Model_User extends ORM + { + ... + protected $_ignored_columns = array('field1', 'field2', ...) + ... + } + +使用 [ORM::values] 方法设置键-值型数组: + + $user->values(array('username' => 'Joe', 'password' => 'bob')); + +### 创建并存储记录 + +[ORM::save] 方法既可以用于创建新记录也可作用于更新现有记录。 + + // 创建新记录 + $user = ORM::factory('user'); + $user->name = 'New user'; + $user->save(); + + // 更新现有记录 + $user = ORM::factory('user', 5); + $user->name = 'User 2'; + $user->save(); + + // 检查记录是否保存成功 + if ($user->saved()) { ... } + +你也可以使用 [ORM::save_all] 方法来更新多条记录: + + $user = ORM::factory('user'); + $user->name = 'Bob'; + + // 更新所有结果记录的名字为 'Bob' + $user->where('active', '=', TRUE)->save_all(); + +#### 使用 `Updated` 和 `Created` 列 + +`_updated_column` 和 `_created_column` 变量是用于当模型更新或插入新纪录的时候自动更新设置的字段值。默认没有使用。如果你想使用: + + // date_created 列用于储存创建的时间,使用 TRUE 保存的是时间戳(timestamp) + protected $_created_column = array('date_created' => TRUE); + + // date_modified 列用于储存最后修改时间。这里的时间设置为使用 date() 格式后的字符串 + protected $_updated_column = array('date_modified' => 'm/d/Y'); + +### 删除记录 + +删除记录可以使用 [ORM::delete] 和 [ORM::delet_all] 方法。这两个方法的使用和上面 存储记录 方法类似,但有一点不同的是 [ORM::delete] 方法带有一个删除记录 'id' 的可选参数。 + +### 关系 + +ORM 提供强大的关系模型。Ruby 有一篇介绍关系模型的文章: [http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html](http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html) + +#### Belongs-To 和 Has-Many + +假设我们在一所学校工作,当然学校有很多学生,而每个学生都只属于一个学校。这样的关系模型可以这样定义: + + // school 模型文件 + protected $_has_many = array('students' => array()); + + // student 模型文件 + protected $_belongs_to = array('school' => array()); + +获取学生的学校: + + $school = $student->school; + +获取学校的学生: + + // 注意在 studends 后必须调用 find_all 方法 + $students = $school->students->find_all(); + + // 缩小范围查询: + $students = $school->students->where('active', '=', TRUE)->find_all(); + +默认情况下,在 student 表定义的模型文件中 ORM 会寻找 `school_id` 当作外键。这个可以通过 `foreign_key` 属性更改: + + protected $_belongs_to = array('school' => array('foreign_key' => 'schoolID')); + +外键应该同时覆写 student 和 school 模型文件。 + +#### Has-One + +Has-One 是 Has-Many 的一个特别情况,唯一不同的这是一对一关系。还以上面的例子说明就是,每个学校有且只有一个学生(当然这是一个很牵强呃例子)。 + + // school 模型文件 + protected $_has_one = array('student' => array()); + +类似于 Belongs-To,当你引用 Has-One 关系对象的时候无需调用 `find` 方法 - 它是自动完成的。 + +#### Has-Many "Through" + +Has-Many "through" 关系(也可以称之为 Has-And-Belongs-To-Many) is used in the case of one object being related to multiple objects of another type, and visa-versa. For instance, a student may have multiple classes and a class may have multiple students. In this case, a third table and model known as a `pivot` is used. In this case, we will call the pivot object/model `enrollment`. + + // student (学生)模型文件 + protected $_has_many = array('classes' => array('through' => 'enrollment')); + + // class (班级)模型文件 + protected $_has_many = array('students' => array('through' => 'enrollment')); + +其中 enrollment 表包含两个外键: `class_id` 和 `student_id`。在定义关系时,使用 `foreign_key` 和 `far_key` 覆写了默认值。例如: + + // student (学生)模型文件() (the foreign key refers to this model [student], while the far key refers to the other model [class]) + protected $_has_many = array('classes' => array('through' => 'enrollment', 'foreign_key' => 'studentID', 'far_key' => 'classID')); + + // class (班级)模型文件 + protected $_has_many = array('students' => array('through' => 'enrollment', 'foreign_key' => 'classID', 'far_key' => 'studentID')); + +enrollment 模型文件应该这样定义: + + // Enrollment 模型同时属于一个 student 和 class + protected $_belongs_to = array('student' => array(), 'class' => array()); + +获取相关对象: + + // 从 student 中获取 classes + $student->classes->find_all(); + + // 从 class 中获取 students + $class->students->find_all(); + +### 校验 + +ORM 和 [Validate] 类是紧密结合使用的。ORM 提供以下几种校验方式: + +* _rules +* _callbacks +* _filters +* _labels + +#### `_rules` + + protected $_rules = array + ( + 'username' => array('not_empty' => array()), + 'email' => array('not_empty' => array(), 'email' => array()), + ); + +检测并确保 `username` 字段不为空。检测 `email` 字段不为空且是有效的 Email 地址格式。那些传递空值数组用于提供可选的额外参数到校验方法中使用。 + +#### `_callbacks` + + protected $_callbacks = array + ( + 'username' => array('username_unique'), + ); + +`username` 字段被传递到了 `username_unique` 回调函数。如果方法存在于当前模型它就会被调用,否则调用全局函数。下面有个小例子: + + public function username_unique(Validate $data, $field) + { + // 确保 username 是唯一的 + ... + } + +#### `_filters` + + protected $_filters = array + ( + TRUE => array('trim' => array()), + 'username' => array('stripslashes' => array()), + ); + +`TRUE` 值代表 `trim` 过滤器应用到所有字段。而 `username` 字段则在校验前使用 `stripslashes` 过滤。那些传递空值数组用于提供可选的额外参数到校验方法中使用。 + +#### 检测对象是否通过校验 + +使用 [ORM::check] 检测当前对象是否通过校验: + + // 设置完对象的值,接下来检测是否通过校验 + if ($user->values($_POST)->check()) + { + $user->save(); + } + +你也可是使用 `validate()` 方法直接访问模型的校验对象: + + // 手动添加额外的过滤器 + $user->validate()->filter('username', 'trim'); \ No newline at end of file diff --git a/includes/kohana/modules/userguide/guide/zh-cn/tutorials.removeindex.md b/includes/kohana/modules/userguide/guide/zh-cn/tutorials.removeindex.md new file mode 100644 index 00000000..b38119f5 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/tutorials.removeindex.md @@ -0,0 +1,89 @@ +# 从 URL 移除 `index.php` + +为了保持 URLs 的干净,你可能希望 URL 在访问的时候不包含 `/index.php/`。下面有两步可以实现: + +1. 编辑 bootstrap 文件 +2. 设置重写规则 + +# 配置 Bootstrap + +首先你需要在 [Kohana::init] 方法中更改 `index_file` 设置: + + Kohana::init(array( + 'base_url' => '/myapp/', + 'index_file' => FALSE, + )); + +现在所有使用 [URL::site],[URL::base] 和 [HTML::anchor] 生成的 URL均不会包含 "index.php" 了。 + +# URL 重写 + +开启重写配置的方法根据服务器的不同而不同,下面仅供参考: + +## Apache + +改名 `example.htaccess` 为 `.htaccess` 后修改下面的参数代码: + + RewriteBase /kohana/ + +这里需要和 [Kohana::init] 方法中的 `base_url` 选项匹配: + + RewriteBase /myapp/ + +完成了,就这点事! + +### 失败了! + +如果提示 "Internal Server Error" 或 "No input file specified" 错误,请尝试下面的修改: + + RewriteRule ^(?:application|modules|system)\b - [F,L] + +相反,我们可以尝试反斜杠: + + RewriteRule ^(application|modules|system)/ - [F,L] + +如果这样还不工作,再试着修改: + + RewriteRule .* index.php/$0 [PT] + +再简单点: + + RewriteRule .* index.php [PT] + +### 仍然失败! + +如果还是提示失败的话,请确保你的服务器支持 URL 的 `mod_rewrite`。 +加入你可以修改 Apache 的配置,你可以复制下面的配置到 `httpd.conf`: + + + Order allow,deny + Allow from all + AllowOverride All + + +## NGINX + +很难给出 nginx 的配置实例,但是修改其实非常简单: + + location / { + index index.php index.html index.htm; + try_files $uri $uri/ index.php$uri?$args; + } + + location ~ ^(.+\.php)(.*)$ { + fastcgi_split_path_info ^(.+\.php)(.*)$; + fastcgi_param SCRIPT_NAME $fastcgi_script_name; + fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + + include fastcgi.conf; + + fastcgi_pass 127.0.0.1:9000; + fastcgi_index index.php; + } + +两点需要注意的是使用 [try_files](http://wiki.nginx.org/NginxHttpCoreModule#try_files) 和 [fastcgi_split_path_info](http://wiki.nginx.org/NginxHttpFcgiModule#fastcgi_split_path_info)。 + +[!!] 以上配置假定你的 PHP 是在端口为 9000 的 FastCGI 服务器,同时 nginx 在 v0.731 以上版本。 + +如果在运行中遇到的问题,请在 nginx 中启用 debug 级别的日志记录并检查 access 和 error 日志。 diff --git a/includes/kohana/modules/userguide/guide/zh-cn/tutorials.urls.md b/includes/kohana/modules/userguide/guide/zh-cn/tutorials.urls.md new file mode 100644 index 00000000..f4360c03 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/tutorials.urls.md @@ -0,0 +1,160 @@ +# 路由,URLs 和链接 + +本节讲述了关于 Kohana 的请求路由, URL 生成以及链接的基本使用。 + +## 路由(Routing) + +在上面提到的[请求流程](about.flow)一节中,一个请求通过 [Request] 类来寻找匹配 [Route] 并且加载对应的控制器以执行请求。本系统提供了更大的灵活性以及常规默认行为。 + +如果你查看了 `APPPATH/bootstrap.php` 的代码,你会发现会有包含下面的一段代码,它会在请求处理对在 [Request::instance] 关闭前立即执行。 + + Route::set('default', '((/(/)))') + ->defaults(array( + 'controller' => 'welcome', + 'action' => 'index', + )); + +这是按照 `((/(/)))` 的 uri 格式化的 ‘默认’ 路由设置。其中 *key* 使用 '<>' 括起来,而*可选*部分使用 '()' 括起来。既然是这样,上面的路由设置说明,所有的 uri 都是可选的,所以如果对于一个空的 uri 要匹配,它会去匹配默认的控制器和 action,也就是上面代码将会匹配并加载 `Controller_Welcome` 类,调用 `action_index` 方法以执行请求。 + +需要注意的是,任何的字符都是允许使用 `()<>` 括起来,对于 `/` 并没有特殊含义。在默认路由中 `/` 是被用来当作静态分隔符,但是如果正确的正则表达式是不会限制你如果格式化你的路由。 + +### 目录 + +对于某些原因你可能需要把一些控制器放置在子目录当作。比如这里有一个 amdin 子目录: + + Route::set('admin', 'admin(/(/(/)))') + ->defaults(array( + 'directory' => 'admin', + 'controller' => 'home', + 'action' => 'index', + )); + +该路由规定了 uri 中必须以 `admin` 开头去匹配,并且默认的,这个目录是静态被分配到 `admin`。如果现在有一个请求到 `admin/users/create` 那么它会加载 `Controller_Admin_Users` 类并调用 `action_create` 方法。 + +### 模式 + +Kohana 路由系统使用 perl 正则表达式来处理匹配。默认情况下 key(使用 `<>` 括起来的)只能根据 `[a-zA-Z0-9_]++` 来匹配,但是你可以为每个 key 以数组的形式自定义不同的模式分配到 [Route::set]。继续扩充上面的例子,如果你之前定义了一个 amdin 和 addiliate 段。其实可以使用路由分割或者下面的方式指定它们: + + Route::set('sections', '(/(/(/)))', + array( + 'directory' => '(admin|affiliate)' + )) + ->defaults(array( + 'controller' => 'home', + 'action' => 'index', + )); + +上面的设置同时实现了两个段的路由映射,'admin' 和 'affiliate' 会映射到相对于的目录控制器里但是它会覆盖默认的路由设置。 + +### 更多路由样例 + +这里还有一些其他使用技巧,下面是一些样例: + + /* + * 验证的缩写 + */ + Route::set('auth', '', + array( + 'action' => '(login|logout)' + )) + ->defaults(array( + 'controller' => 'auth' + )); + + /* + * 多样式 feeds + * 452346/comments.rss + * 5373.json + */ + Route::set('feeds', '(/).', + array( + 'user_id' => '\d+', + 'format' => '(rss|atom|json)', + )) + ->defaults(array( + 'controller' => 'feeds', + 'action' => 'status', + )); + + /* + * 静态页面 + */ + Route::set('static', '.html', + array( + 'path' => '[a-zA-Z0-9_/]+', + )) + ->defaults(array( + 'controller' => 'static', + 'action' => 'index', + )); + + /* + * 你不喜欢斜线号?那我们使用冒号分隔。 + * EditGallery:bahamas + * Watch:wakeboarding + */ + Route::set('gallery', '():', + array( + 'controller' => '[A-Z][a-z]++', + 'action' => '[A-Z][a-z]++', + )) + ->defaults(array( + 'controller' => 'Slideshow', + )); + + /* + * 快速搜索 + */ + Route::set('search', ':', array('query' => '.*')) + ->defaults(array( + 'controller' => 'search', + 'action' => 'index', + )); + +路由的匹配是按照顺序指定的所以大家需要知道的是,如果你在加载模块之后设置路由,模块也可以指定路由程序相冲突的路由。如果是因为这个为什么默认路由会在最后设置,所以字段能够以路由的时候最好先做测试。 + +### 请求参数 + +目录,控制器和 action 都可以通过 [Request] 实例化后的两种方式访问: + + $this->request->action; + Request::instance()->action; + +所有其他定义在路由中的键值可以从内控制器中访问: + + $this->request->param('key_name'); + +[Request::param] 方法提供一个可选的第二参数,用于返回默认的没有找到路由设置键值的值。如果没有指定第二参数,返回包含所有键值的数组。 + +### 约定 + +约定适用于自定义的扩展的 `MODPATH//init.php` 文件或者 `APPPATH/bootstrap.php` 文件默认设置的路由。当然,你也可以采用外部加载,甚至是动态加载的方式。 + +## URLs + +随着 Kohana 路由功能的不断强大,加入了一些生成路由 URI 的方法。通常你可能在调用 [URL::site] 方法时指定的字符串来创建完整的 URL: + + URL::site('admin/edit/user/'.$user_id); + +同时,Kohana 也提供另外一种从路由定义生成 URI 的方法。假如能够所以改变的路由的参数从而减轻代码的变更带来的烦恼,这是非常好的替代方法。下面提供一个使用 `feeds` 路由动态生成 URL 的例子: + + Route::get('feeds')->uri(array( + 'user_id' => $user_id, + 'action' => 'comments', + 'format' => 'rss' + )); + +比方说,你今后决定改变 `feeds/(/).` 的路由定义作进一步的设计。 +Let's say you decided later to make that route definition more verbose by changing it to `feeds/(/).`. If you wrote your code with the above uri generation method you wouldn't have to change a single line! When a part of the uri is enclosed in parentheses and specifies a key for which there in no value provided for uri generation and no default value specified in the route, then that part will be removed from the uri. An example of this is the `(/)` part of the default route; this will not be included in the generated uri if an id is not provided. + +[Request::uri] 可能会是你经常使用的方法,它除了上面说明的功能外还可以设定当前的路由,目录,控制器和 actions 的值。如果我们当前的默认路由是 `users/list`,我们可以生成这样的格式 `users/view/$id`: + + $this->request->uri(array('action' => 'view', 'id' => $user_id)); + +或者在视图中,可取的方法: + + Request::instance()->uri(array('action' => 'view', 'id' => $user_id)); + +## 链接 + +[!!] links stub diff --git a/includes/kohana/modules/userguide/guide/zh-cn/using.autoloading.md b/includes/kohana/modules/userguide/guide/zh-cn/using.autoloading.md new file mode 100644 index 00000000..df2f83a0 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/using.autoloading.md @@ -0,0 +1,95 @@ +# 类的加载 + +Kohana 需要使用 PHP 自身的[自动加载](http://php.net/manual/language.oop5.autoload.php)。这个消除了不用调用 [include](http://php.net/include) 和 [require](http://php.net/require) 之前就可以使用类文件。例如,让你想使用 [Cookie::set] 方法时,你只需要: + + Cookie::set('mycookie', 'any string value'); + +或者要加载一个 [Encrypt] 的实例化,只需要调用 [Encrypt::instance]: + + $encrypt = Encrypt::instance(); + +类也可以通过 [Kohana::auto_load] 方法加载,这使得从简单的类名称转换为文件名: + +1. 类必须放置在[文件系统](start.filesystem)的 `classes/` 目录 +2. 任何下划线字符转换为斜线 +2. 文件名必须是小写的 + +当调用一个尚未加载类(比如,`Session_Cookie`),通过使用 [Kohana::find_file] 方法可以让 Kohana 搜索文件系统查找名为 `classes/session/cookie.php` 的文件。 + +## 自动加载器 + +在 `application/bootstrap.php` 配置文件默认使用 [spl_autoload_register](http://php.net/spl_autoload_register) 开启了自动加载器。 + + spl_autoload_register(array('Kohana', 'auto_load')); + +在此类第一次使用的时候,这让 [Kohana::auto_load] 尝试去加载任意的不存在类。 + +# Transparent Class Extension {#class-extension} + +The [cascading filesystem](about.filesystem) allows transparent class extension. For instance, the class [Cookie] is defined in `SYSPATH/classes/cookie.php` as: + + class Cookie extends Kohana_Cookie {} + +The default Kohana classes, and many extensions, use this definition so that almost all classes can be extended. You extend any class transparently, by defining your own class in `APPPATH/classes/cookie.php` to add your own methods. + +[!!] You should **never** modify any of the files that are distributed with Kohana. Always make modifications to classes using extensions to prevent upgrade issues. + +For instance, if you wanted to create method that sets encrypted cookies using the [Encrypt] class: + + encode((string) $value); + + parent::set($name, $value, $expiration); + } + + /** + * Gets an encrypted cookie. + * + * @uses Cookie::get + * @uses Encrypt::decode + */ + public static function decrypt($name, $default = NULL) + { + if ($value = parent::get($name, NULL)) + { + $value = Encrypt::instance(Cookie::$encryption)->decode($value); + } + + return isset($value) ? $value : $default; + } + + } // End Cookie + +Now calling `Cookie::encrypt('secret', $data)` will create an encrypted cookie which we can decrypt with `$data = Cookie::decrypt('secret')`. + +## Multiple Levels of Extension {#multiple-extensions} + +If you are extending a Kohana class in a module, you should maintain transparent extensions. Instead of making the [Cookie] extension extend Kohana, you can create `MODPATH/mymod/encrypted/cookie.php`: + + class Encrypted_Cookie extends Kohana_Cookie { + + // Use the same encrypt() and decrypt() methods as above + + } + +And create `MODPATH/mymod/cookie.php`: + + class Cookie extends Encrypted_Cookie {} + +This will still allow users to add their own extension to [Cookie] with your extensions intact. However, the next extension of [Cookie] will have to extend `Encrypted_Cookie` instead of `Kohana_Cookie`. diff --git a/includes/kohana/modules/userguide/guide/zh-cn/using.configuration.md b/includes/kohana/modules/userguide/guide/zh-cn/using.configuration.md new file mode 100644 index 00000000..0a272412 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/using.configuration.md @@ -0,0 +1,57 @@ +# 通用配置 + +Kohana uses both static properties and files for configuration. Static properties are typically used for static classes, such as [Cookie], [Security], and [Upload]. Files are typically used for objects such as [Database], [Encrypt], and [Session]. + +Static properties can be set in `APPPATH/bootstrap.php` or by [class extension](using.autoloading#class-extension). The benefit of static properties is that no additional files need to be loaded. The problem with this method is that it causes the class to be loaded when the property is set, if you do not use an extension. However, using extensions will overload extensions made in modules. It is generally recommended to do static property configuration in the bootstrap. + +[!!] When using opcode caching, such as [APC](http://php.net/apc) or [eAccelerator](http://eaccelerator.net/), class loading time is significantly reduced. It is highly recommended to use opcode caching with *any* production website, no matter the size. + +## 加载配置 + +Every new Kohana installation will require changing [Kohana::init] settings in `APPPATH/bootstrap.php`. Any setting that is not set will use the default setting. These settings can be accessed and modified later by using the static property of the [Kohana] class. For instance, to get the current character set, read the [Kohana::$charset] property. + +## 安全配置 + +There are several settings which need to be changed to make Kohana secure. The most important of these is [Cookie::$salt], which is used to create a "signature" on cookies that prevents them from being modified outside of Kohana. + +If you plan to use the [Encrypt] class, you will also need to create an `encrypt` configuration file and set the encryption `key` value. The encryption key should include letters, numbers, and symbols for the best security. + +[!!] **Do not use a hash for the encryption key!** Doing so will make the encryption key much easier to crack. + +# 配置文件 {#config-files} + +Configuration files are slightly different from other files within the [cascading filesystem](about.filesystem) in that they are **merged** rather than overloaded. This means that all configuration files with the same file path are combined to produce the final configuration. The end result is that you can overload *individual* settings rather than duplicating an entire file. + +配置文件存放在 `config/` 目录的 PHP 文件,结构类似于: + + 'value', + 'options' => array( + 'foo' => 'bar', + ), + ); + +如果上面的配置文件名为 `myconf.php`,你可以通过下面代码调用: + + $config = Kohana::config('myconf'); + $options = $config['options']; + +[Kohana::config] 也提供了一钟使用“逗号格式”访问配置数组中的键: + +获得 "options" 数组: + + $options = Kohana::config('myconf.options'); + +从 "options" 数组获得 "foo" 键: + + $foo = Kohana::config('myconf.options.foo'); + +配置数组也可以当作对象访问,如果你喜欢下面的方法: + + $options = Kohana::config('myconf')->options; + +请注意的是你只能使用键值型数组访问首个变量,其余的子键都必须使用标准数组方式访: + + $foo = Kohana::config('myconf')->options['foo']; diff --git a/includes/kohana/modules/userguide/guide/zh-cn/using.messages.md b/includes/kohana/modules/userguide/guide/zh-cn/using.messages.md new file mode 100644 index 00000000..3d223609 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/using.messages.md @@ -0,0 +1,26 @@ +# 消息的基本使用 + +Kohana 消息(messages) 是一种友好化短小的词或短语的字符串,通常被叫做 "key"。消息通过 [Kohana::message] 方法调用访问并返回整个消息组或者单个消息。 + +举个简单的例子,当用户没有登录并试图访问一个需要验证的页面,通常会一个类似"你必须登录后才能访问此页面"的提示,而此消息可以保存在 auth 文件的 'must_login' 的键值中: + + $message = Kohana::message('auth', 'must_login'); + +消息并不能直接翻译,如果想翻译一个消息,你需要配合使用[翻译函数](using.translation): + + $translated = __(Kohana::message('auth', 'must_login')); + +[!!] 在 Kohana v2 版本中,消息系统是可以翻译的,尽管如此,我们还是强烈推荐大家使用新的翻译系统代替消息,因为当翻译不存时它可以提供可读性文本。 + +## 消息文件 + +所有的消息文件都是保存在 `messages/` 目录下的纯 PHP 文件的配对数组: + + '你必须登录后才能访问此页面', + 'no_access' => '你没有访问此页面的权限', + ); + +消息文件有些类似于[配置文件](using.configuration#config-files),它们都可以合并在一起。这意味着所有的消息都可以设置为一个数组并保存在 'auth' 文件之中。因此当你需要一个新的 'auth' 文件而没有必要创建多个重复文件。 diff --git a/includes/kohana/modules/userguide/guide/zh-cn/using.sessions.md b/includes/kohana/modules/userguide/guide/zh-cn/using.sessions.md new file mode 100644 index 00000000..7b7f4591 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/using.sessions.md @@ -0,0 +1,223 @@ +# Using Sessions and Cookies + +Kohana provides a couple of classes that make it easy to work with both cookies and session. At a high level both sessions and cookies provide the same function. They allow the developer to store temporary or persistent information about a specific client for later retrieval. + +Cookies should be used for storing non-private data that is persistent for a long period of time. For example storing a user id or a language preference. Use the [Cookie] class for getting and setting cookies. + +[!!] Kohana uses "signed" cookies. Every cookie that is stored is combined with a secure hash to prevent modification of the cookie. This hash is generated using [Cookie::salt], which uses the [Cookie::$salt] property. You should [change this setting](using.configuration) when your application is live. + +Sessions should be used for storing temporary or private data. Very sensitive data should be stored using the [Session] class with the "database" or "native" adapters. When using the "cookie" adapter, the session should always be encrypted. + +[!!] For more information on best practices with session variables see [the seven deadly sins of sessions](http://lists.nyphp.org/pipermail/talk/2006-December/020358.html). + +# Storing, Retrieving, and Deleting Data + +[Cookie] and [Session] provide a very similar API for storing data. The main difference between them is that sessions are accessed using an object, and cookies are accessed using a static class. + +Accessing the session instance is done using the [Session::instance] method: + + // Get the session instance + $session = Session::instance(); + +When using sessions, you can also get all of the current session data using the [Session::as_array] method: + + // Get all of the session data as an array + $data = $session->as_array(); + +You can also use this to overload the `$_SESSION` global to get and set data in a way more similar to standard PHP: + + // Overload $_SESSION with the session data + $_SESSION =& $session->as_array(); + + // Set session data + $_SESSION[$key] = $value; + +## Storing Data {#setting} + +Storing session or cookie data is done using the `set` method: + + // Set session data + $session->set($key, $value); + + // Set cookie data + Cookie::set($key, $value); + + // Store a user id + $session->set('user_id', 10); + Cookie::set('user_id', 10); + +## Retrieving Data {#getting} + +Getting session or cookie data is done using the `get` method: + + // Get session data + $data = $session->get($key, $default_value); + + // Get cookie data + $data = Cookie::get($key, $default_value); + + // Get the user id + $user = $session->get('user_id'); + $user = Cookie::get('user_id'); + +## Deleting Data {#deleting} + +Deleting session or cookie data is done using the `delete` method: + + // Delete session data + $session->delete($key); + + // Delete cookie data + Cookie::delete($key); + + // Delete the user id + $session->delete('user_id'); + Cookie::delete('user_id'); + +# Configuration {#configuration} + +Both cookies and sessions have several configuration settings which affect how data is stored. Always check these settings before making your application live, as many of them will have a direct affect on the security of your application. + +## Cookie Settings + +All of the cookie settings are changed using static properties. You can either change these settings in `bootstrap.php` or by using a [class extension](using.autoloading#class-extension). + +The most important setting is [Cookie::$salt], which is used for secure signing. This value should be changed and kept secret: + + Cookie::$salt = 'your secret is safe with me'; + +[!!] Changing this value will render all cookies that have been set before invalid. + +By default, cookies are stored until the browser is closed. To use a specific lifetime, change the [Cookie::$expiration] setting: + + // Set cookies to expire after 1 week + Cookie::$expiration = 604800; + + // Alternative to using raw integers, for better clarity + Cookie::$expiration = Date::WEEK; + +The path that the cookie can be accessed from can be restricted using the [Cookie::$path] setting. + + // Allow cookies only when going to /public/* + Cookie::$path = '/public/'; + +The domain that the cookie can be accessed from can also be restricted, using the [Cookie::$domain] setting. + + // Allow cookies only on the domain www.example.com + Cookie::$domain = 'www.example.com'; + +If you want to make the cookie accessible on all subdomains, use a dot at the beginning of the domain. + + // Allow cookies to be accessed on example.com and *.example.com + Cookie::$domain = '.example.com'; + +To only allow the cookie to be accessed over a secure (HTTPS) connection, use the [Cookie::$secure] setting. + + // Allow cookies to be accessed only on a secure connection + Cookie::$secure = TRUE; + + // Allow cookies to be accessed on any connection + Cookie::$secure = FALSE; + +To prevent cookies from being accessed using Javascript, you can change the [Cookie::$httponly] setting. + + // Make cookies inaccessible to Javascript + Cookie::$httponly = TRUE; + +## Session Adapters {#adapters} + +When creating or accessing an instance of the [Session] class you can decide which session adapter you wish to use. The session adapters that are available to you are: + +Native +: Stores session data in the default location for your web server. The storage location is defined by [session.save_path](http://php.net/manual/session.configuration.php#ini.session.save-path) in `php.ini` or defined by [ini_set](http://php.net/ini_set). + +Database +: Stores session data in a database table using the [Session_Database] class. Requires the [Database] module to be enabled. + +Cookie +: Stores session data in a cookie using the [Cookie] class. **Sessions will have a 4KB limit when using this adapter.** + +The default datapter can be set by changing the value of [Session::$default]. The default adapter is "native". + +[!!] As with cookies, a "lifetime" setting of "0" means that the session will expire when the browser is closed. + +### Session Adapter Settings + +You can apply configuration settings to each of the session adapters by creating a session config file at `APPPATH/config/session.php`. The following sample configuration file defines all the settings for each adapater: + + return array( + 'native' => array( + 'name' => 'session_name', + 'lifetime' => 43200, + ), + 'cookie' => array( + 'name' => 'cookie_name', + 'encrypted' => TRUE, + 'lifetime' => 43200, + ), + 'database' => array( + 'name' => 'cookie_name', + 'encrypted' => TRUE, + 'lifetime' => 43200, + 'group' => 'default', + 'table' => 'table_name', + 'columns' => array( + 'session_id' => 'session_id', + 'last_active' => 'last_active', + 'contents' => 'contents' + ), + 'gc' => 500, + ), + ); + +#### Native Adapter {#adapter-native} + +Type | Setting | Description | Default +----------|-----------|---------------------------------------------------|----------- +`string` | name | name of the session | `"session"` +`integer` | lifetime | number of seconds the session should live for | `0` + +#### Cookie Adapter {#adapter-cookie} + +Type | Setting | Description | Default +----------|-----------|---------------------------------------------------|----------- +`string` | name | name of the cookie used to store the session data | `"session"` +`boolean` | encrypted | encrypt the session data using [Encrypt]? | `FALSE` +`integer` | lifetime | number of seconds the session should live for | `0` + +#### Database Adapter {#adapter-database} + +Type | Setting | Description | Default +----------|-----------|---------------------------------------------------|----------- +`string` | group | [Database::instance] group name | `"default"` +`string` | table | table name to store sessions in | `"sessions"` +`array` | columns | associative array of column aliases | `array` +`integer` | gc | 1:x chance that garbage collection will be run | `500` +`string` | name | name of the cookie used to store the session data | `"session"` +`boolean` | encrypted | encrypt the session data using [Encrypt]? | `FALSE` +`integer` | lifetime | number of seconds the session should live for | `0` + +##### Table Schema + +You will need to create the session storage table in the database. This is the default schema: + + CREATE TABLE `sessions` ( + `session_id` VARCHAR(24) NOT NULL, + `last_active` INT UNSIGNED NOT NULL, + `contents` TEXT NOT NULL, + PRIMARY KEY (`session_id`), + INDEX (`last_active`) + ) ENGINE = MYISAM; + +##### Table Columns + +You can change the column names to match an existing database schema when connecting to a legacy session table. The default value is the same as the key value. + +session_id +: the name of the "id" column + +last_active +: UNIX timestamp of the last time the session was updated + +contents +: session data stored as a serialized string, and optionally encrypted diff --git a/includes/kohana/modules/userguide/guide/zh-cn/using.views.md b/includes/kohana/modules/userguide/guide/zh-cn/using.views.md new file mode 100644 index 00000000..54f9c220 --- /dev/null +++ b/includes/kohana/modules/userguide/guide/zh-cn/using.views.md @@ -0,0 +1,118 @@ +# 视图的使用 + +视图是包含输出显示信息内容的文件。通常大多数情况下是 HTML,CSS 和 Javascript 或者其他任何内容(包括调用 AJAX 的 XML 或 JSON 的输出)。其主要目的是为了从程序中分离逻辑以获得可复用性和整洁代码。 + +然而事实上,视图本身也能传递变量等代码并输出数据。比如,循环产品信息的数组并输出每个产品的信息。视图仍然是 PHP 文件因此你可以正常的写任何代码。 + +# 创建视图文件 + +视图文件存在[文件系统](about.filesystem)中的 `views` 目录。你也可以在 `views` 目录下面创建子目录组织你的文件。下面所有的例子都是合理的视图文件: + + APPPATH/views/home.php + APPPATH/views/pages/about.php + APPPATH/views/products/details.php + MODPATH/error/views/errors/404.php + MODPATH/common/views/template.php + +## 加载视图 + +[View] 对象通常在 [Controller] 内部通过使用 [View::factory] 方法创建。一般视图被赋值给 [Request::$response] 属性或其他视图。 + + public function action_about() + { + $this->request->response = View::factory('pages/about'); + } + +当视图对象如同上面的例子赋值给 [Request::$response],必要时它会自动输出呈现。如果想获得视图输出的内容,你可以调用 [View::render] 方法或者强制转为字符串类型。当时视图输出呈现时,视图会被加载并生成 HTML 代码。 + + public function action_index() + { + $view = View::factory('pages/about'); + + // Render the view + $about_page = $view->render(); + + // Or just type cast it to a string + $about_page = (string) $view; + + $this->request->response = $about_page; + } + +## 视图变量 + +一旦视图已经被加载,我们可以通过 [View::set] 和 [View::bind] 方法赋值变量。 + + public function action_roadtrip() + { + $view = View::factory('user/roadtrip') + ->set('places', array('Rome', 'Paris', 'London', 'New York', 'Tokyo')); + ->bind('user', $this->user); + + // 视图拥有 $places 和 $user 变量 + $this->request->response = $view; + } + +[!!] `set()` 和 `bind()` 方法的区别在于 `bind()` 是引用赋值。如果你在变量定义之前使用 `bind()` 绑定了它。变量默认会被当作 `NULL` 创建。 + +### 全局变量 + +在程序中可能有多个视图文件而同时调用同样的变量。比如,在两个模板的 header 块中显示一个页面的相同标题而不同的内容。通过 [View::set_global] 和 [View::bind_global] 方法创建全局变量。 + + // 赋值 $page_title 到所有的视图 + View::bind_global('page_title', $page_title); + +假如程序中首页有三个视图需要输出呈现:`template`,`template/sidebar` 和 `pages/home`。首先,创建一个抽象类控制器去初始化视图模板: + + abstract class Controller_Website extends Controller_Template { + + public $page_title; + + public function before() + { + parent::before(); + + // 定义 $page_title 变量到所有视图中使用 + View::bind_global('page_title', $this->page_title); + + // 加载视图为 $sidebar 变量到模板 + $this->template->sidebar = View::factory('template/sidebar'); + } + + } + +下一步,在 home 控制器继承 `Controller_Website`: + + class Controller_Home extends Controller_Website { + + public function action_index() + { + $this->page_title = 'Home'; + + $this->template->content = View::factory('pages/home'); + } + + } + +## 视图嵌套 + +如果你想在视图中加载另外一个视图,这里提供两个方案。通过调用 [View::factory] 你可以实现沙盒加载视图。这意味着你可以使用 [View::set] 或 [View::bind] 赋值: + + // 只有 $user 变量可用在 "views/user/login.php" 视图文件 + bind('user', $user) ?> + +另外一种选择是直接加载视图,这会使得当前所有变量加载并在视图中使用: + + // 所有定义在此视图中的变量都会加载到 "views/message.php" 文件 + + +另外,你也可以在整个 [Request] 中加载一个视图中: + + execute() ?> + +这是一个 [HMVC](about.mvc) 的例子已确保它可以创建并从程序其他的 URL 调用。 + +# 升级 v2.x 版本 + +不同于 Kohana v2.x 版本,视图不在 [Controller] 环境中加载,因此你不能够把 `$this` 当作加载视图的控制器访问。传递控制器到视图必须这样实现: + + $view->bind('controller', $this); diff --git a/includes/kohana/modules/userguide/i18n/de.php b/includes/kohana/modules/userguide/i18n/de.php new file mode 100644 index 00000000..836f6635 --- /dev/null +++ b/includes/kohana/modules/userguide/i18n/de.php @@ -0,0 +1,6 @@ + 'Handbuch' +); diff --git a/includes/kohana/modules/userguide/i18n/es.php b/includes/kohana/modules/userguide/i18n/es.php new file mode 100644 index 00000000..22175dc5 --- /dev/null +++ b/includes/kohana/modules/userguide/i18n/es.php @@ -0,0 +1,6 @@ + 'Guía de Usuario' +); diff --git a/includes/kohana/modules/userguide/i18n/fr.php b/includes/kohana/modules/userguide/i18n/fr.php new file mode 100644 index 00000000..017d6a3a --- /dev/null +++ b/includes/kohana/modules/userguide/i18n/fr.php @@ -0,0 +1,7 @@ + 'Guide Utilisateur' +); + diff --git a/includes/kohana/modules/userguide/i18n/he.php b/includes/kohana/modules/userguide/i18n/he.php new file mode 100644 index 00000000..0d392ed5 --- /dev/null +++ b/includes/kohana/modules/userguide/i18n/he.php @@ -0,0 +1,6 @@ + 'מדריך למשתמש' +); diff --git a/includes/kohana/modules/userguide/i18n/nl.php b/includes/kohana/modules/userguide/i18n/nl.php new file mode 100644 index 00000000..c93ebf52 --- /dev/null +++ b/includes/kohana/modules/userguide/i18n/nl.php @@ -0,0 +1,6 @@ + 'Gebruiksaanwijzing' +); \ No newline at end of file diff --git a/includes/kohana/modules/userguide/i18n/ru.php b/includes/kohana/modules/userguide/i18n/ru.php new file mode 100644 index 00000000..ecf4ce08 --- /dev/null +++ b/includes/kohana/modules/userguide/i18n/ru.php @@ -0,0 +1,7 @@ + 'Руководство пользователя' +); + diff --git a/includes/kohana/modules/userguide/i18n/zh.php b/includes/kohana/modules/userguide/i18n/zh.php new file mode 100644 index 00000000..6025fdda --- /dev/null +++ b/includes/kohana/modules/userguide/i18n/zh.php @@ -0,0 +1,28 @@ + '用户手册', + + // Errors + 'Error' => '错误', + 'Userguide page not found' => '用户手册页面无法找到', + 'API Reference: Class not found.' => 'API 参考: 没有找到此类。', + 'That class is hidden' => '那个是隐藏类', + + // API + 'Table of Contents' => '目录', + 'Available Classes' => '可用的类', + 'Class Contents' => '类列表', + 'Constants' => '常量', + 'Properties' => '属性', + 'Methods' => '方法', + 'None' => '无', + 'Parameters' => '参数', + 'Parameter' => '参数', + 'Type' => '类型', + 'Description' => '描述', + 'Default' => '默认', + 'Return Values' => '返回值', + 'Source Code' => '源代码', +); diff --git a/includes/kohana/modules/userguide/init.php b/includes/kohana/modules/userguide/init.php new file mode 100644 index 00000000..e55f947c --- /dev/null +++ b/includes/kohana/modules/userguide/init.php @@ -0,0 +1,30 @@ +)', array('file' => '.+')) + ->defaults(array( + 'controller' => 'userguide', + 'action' => 'media', + 'file' => NULL, + )); + +if (Kohana::config('userguide.api_browser') === TRUE) +{ + // API Browser + Route::set('docs/api', 'guide/api(/)', array('class' => '[a-zA-Z0-9_]+')) + ->defaults(array( + 'controller' => 'userguide', + 'action' => 'api', + 'class' => NULL, + )); +} + +// Translated user guide +Route::set('docs/guide', 'guide(/)', array( + 'page' => '.+', + )) + ->defaults(array( + 'controller' => 'userguide', + 'action' => 'docs', + )); + diff --git a/includes/kohana/modules/userguide/media/css/kodoc.css b/includes/kohana/modules/userguide/media/css/kodoc.css new file mode 100644 index 00000000..63ac6bf4 --- /dev/null +++ b/includes/kohana/modules/userguide/media/css/kodoc.css @@ -0,0 +1,62 @@ +html { margin: 0; } +body { margin: 0 } +body.he { direction: rtl } +pre { background: #eee; padding: 0.5em 0.6em 0.4em; overflow: auto; } +table th { color: #444; background: #eee; } +table td { background: #fafafa; } + table tr.alt td { background: #fff; } + +.container .toggle { font-size: 0.7em; float: right; padding: 0 1em; cursor: pointer; color: #777; text-decoration: none; } +.container table { width: 100%; } + +#topbar { padding: 1em 0; margin-bottom: 2em; background: #111; border-bottom: solid 0.2em #eee; color: #ccc; } + #topbar ul.breadcrumb { display: block; margin: 0; padding: 0; overflow: auto; } + #topbar ul.breadcrumb li { display: block; float: left; margin: 0; padding: 0; padding-left: 0.6em; } + #topbar ul.breadcrumb li:before { content: '» '; color: #888; } + #topbar ul.breadcrumb li a { color: #ccc; text-decoration: underline; } + #topbar form { margin: 0 1em; padding: 0; } + #topbar form select { width: 98%; } + +#menu { background: #eee; padding: 1em 0; } + #menu ol, + #menu ul { margin: 0; padding: 0; list-style: none; } + #menu li { margin: 0; padding: 0; } + #menu li { display: block; } + #menu li strong { display: block; margin-bottom: 0.4em; padding: 0.2em 0.4em; font-weight: normal; color: #eee; background: #111; cursor: pointer; } + #menu li li strong { background-color: #ccc; color: #111; } + #menu ol { padding: 0 0.6em; margin: 0; } + #menu ol ul, + #menu ol ol { margin-left: 1em; padding: 0 0 0.6em; } + #menu h3 { font-size: 1em; margin: 1em 0 0; padding: 1em 1em 0.5em; border-top: 5px solid #fff; } + +#footer { margin-top: 1em; padding: 1em 0; color: #666; } + #footer p { font-size: 0.8em; text-transform: uppercase; } +#content h1 { font-size: 3em; letter-spacing: -0.02em; } +#content h2 { margin-top: 1.5em; margin-left: -20px; padding: 0.6em 20px 0.5em 20px; background: #e5e5e5; font-size: 2em; letter-spacing: -0.01em; text-shadow: 0 -1px 0 #fff; } +#content h3 { border-bottom: 2px solid #eee; font-size: 1.6em; } +#content h1 small, +#content h2 small, +#content h3 small, +#content h4 small, +#content h5 small, +#content h6 small { font-size: 0.8em; font-weight: normal; color: #444; } +#content pre { font-size: 1em; line-height: 1.4em; direction: ltr } + +#content div.class-list div.class { width: 50%; } +#content div.class-list div.class.left { float: left; clear: both; } +#content div.class-list div.class.right { float: right; } + #content div.class-list div.class h2 { margin: 0.4em; padding: 0.2em 0.4em; font-size: 1em; } + +#content div.toc { margin-bottom: 1em; padding-bottom: 1em; background: #eee; } + #content div.toc ul { margin: 0.6em 1em; padding: 0; list-style: none; } + #content div.toc li { margin: 0; padding: 0; } + #content div.toc h3 { margin: 0; padding: 0.2em 1em; font-size: 1em; color: #eee; background: #111; } +#content div.method, +#content div.properties, +#content div.constants { margin-bottom: 1em; } + #content div.method h3 .param { font-weight: normal; cursor: help; border-bottom:1px dashed #666;} + #content div.method h3 abbr.param { text-transform: none; font-size: 1em; } +#content p.note { display: block; padding: 0.4em 0.6em; padding-left: 3em; background: url(../img/note.png) 1em 0.6em no-repeat; border: 1px solid #e5e5e5; font-family: Georgia, serif; font-size: 1.05em; font-style: italic; color: #555; } +#content dl.tags { overflow: auto; background: #eee; padding: 1em; border: solid 6px #ddd; } + #content dl.tags dt { margin: 0 0 0.4em; clear: both; float: left; width: 25%; } + #content dl.tags dd { margin: 0; padding: 0; clear: right; float: right; } diff --git a/includes/kohana/modules/userguide/media/css/print.css b/includes/kohana/modules/userguide/media/css/print.css new file mode 100644 index 00000000..8b5ba48f --- /dev/null +++ b/includes/kohana/modules/userguide/media/css/print.css @@ -0,0 +1,46 @@ +/* -------------------------------------------------------------- + + * print.css - BlueTrip CSS Framework + * Thanks to Hartija Print Framework + +-------------------------------------------------------------- */ + +body { +width:100% !important; +margin:0 !important; +padding:0 !important; +line-height: 1.4; +word-spacing:1.1pt; +letter-spacing:0.2pt; font-family: Garamond,"Times New Roman", serif; color: #000; background: none; font-size: 12pt; } + +/*Headings */ +h1,h2,h3,h4,h5,h6 { font-family: Helvetica, Arial, sans-serif; } +h1{font-size:19pt;} +h2{font-size:17pt;} +h3{font-size:15pt;} +h4,h5,h6{font-size:12pt;} + +code { font: 10pt Courier, monospace; } +blockquote { margin: 1.3em; padding: 1em; font-size: 10pt; } +hr { background-color: #ccc; } + +/* Images */ +img { float: left; margin: 1em 1.5em 1.5em 0; } +a img { border: none; } + +/* Links */ +a:link, a:visited { background: transparent; font-weight: 700; text-decoration: underline;color:#333; } + +/* Table */ +table { margin: 1px; text-align:left; } +th { border-bottom: 1px solid #333; font-weight: bold; } +td { border-bottom: 1px solid #333; } +th,td { padding: 4px 10px 4px 0; } +tfoot { font-style: italic; } +caption { background: #fff; margin-bottom:2em; text-align:left; } +thead {display: table-header-group;} +tr {page-break-inside: avoid;} + +/*add sections here to hide various parts from the site*/ + +/*#header, #footer, #navigation, #sidebar, button, a.button {display:none;}*/ \ No newline at end of file diff --git a/includes/kohana/modules/userguide/media/css/screen.css b/includes/kohana/modules/userguide/media/css/screen.css new file mode 100644 index 00000000..d02df5d2 --- /dev/null +++ b/includes/kohana/modules/userguide/media/css/screen.css @@ -0,0 +1,286 @@ +/* ----------------------------------------------------------------------- + + BlueTrip CSS Framework + + Mike Crittenden + mike@capsizedesigns.com + Copyright 2008 Mike Crittenden + + License - MIT or GPL (whichever suits you better) + +----------------------------------------------------------------------- */ + +/* MEYER RESET v1.0*/ + +html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent}body{line-height:1}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}:focus{outline:0}ins{text-decoration:none}del{text-decoration:line-through}table{border-collapse:collapse;border-spacing:0} + +/* BASIC TYPOGRAPHY */ + +html { font-size: 62.5%; font-family: "Liberation Sans", Helvetica, Arial, sans-serif; } +strong, th, thead td, h1, h2, h3, h4, h5, h6 { font-weight: bold; } +cite, em, dfn { font-style: italic; } +code, kbd, samp, pre, tt, var, input[type='text'], input[type='password'], textarea { font-size: 92%; font-family: monaco, "Lucida Console", courier, monospace; } +del { text-decoration: line-through; color: #666; } +ins, dfn { border-bottom: 1px solid #ccc; } +small, sup, sub { font-size: 85%; } +abbr, acronym { text-transform: uppercase; font-size: 85%; letter-spacing: .1em; } +a abbr, a acronym { border: none; } +abbr[title], acronym[title], dfn[title] { cursor: help; border-bottom: 1px solid #ccc; } +sup { vertical-align: super; } +sub { vertical-align: sub; } + +/* QUOTES */ + +blockquote { border-top: 1px solid #ccc; border-bottom: 1px solid #ccc; color: #666; } +blockquote *:first-child:before { content: "\201C"; } +blockquote *:first-child:after { content: "\201D"; } + +/* FORMS */ + +fieldset { padding:1.4em; margin: 0 0 1.5em 0; border: 1px solid #ccc; } +legend { font-weight: bold; font-size:1.2em; } +label { font-weight: bold; } +textarea, input[type='text'], input[type='password'], select { border: 1px solid #ccc; background: #fff; } +textarea:hover, input[type='text']:hover, input[type='password']:hover, select:hover { border-color: #aaa; } +textarea:focus, input[type='text']:focus, input[type='password']:focus, select:focus { border-color: #888; outline: 2px solid #ffffaa; } +input, select { cursor: pointer; } +input[type='text'],input[type='password'] { cursor: text; } + +/* BASE SIZES */ + +.container { font-size: 1.2em; line-height: 1.6em; } +h1 { font-size: 1.9em; } +h2 { font-size: 1.7em; } +h3 { font-size: 1.5em; } +h4 { font-size: 1.3em; } +h5 { font-size: 1.2em; } +h6 { font-size: 1em; } + +/* LISTS */ + +ul li { margin-left: .85em; } +ul { list-style-type: disc; } +ul ul { list-style-type: square; } +ul ul ul { list-style-type: circle; } +ol { list-style-position: outside; list-style-type: decimal; } +dt { font-weight: bold; } + +/* TABLES */ + +table { border-top: 1px solid #ccc; border-left: 1px solid #ccc; } +th, td { border-bottom: 1px solid #ddd; border-right: 1px solid #ccc; } + +/* MARGINS & PADDINGS */ + +blockquote *:first-child { margin: .8em 0; } +hr, p, ul, ol, dl, pre, blockquote, address, table, form { margin-bottom: 1.6em; } +/* NOTE: Calulate header margins: TOP: 1.6em/size, BOTTOM: 1.6em/size/2 */ +h1 { margin: 1em 0 .5em; } +h2 { margin: 1.07em 0 .535em; } +h3 { margin: 1.14em 0 .57em; } +h4 { margin: 1.23em 0 .615em; } +h5 { margin: 1.33em 0 .67em; } +h6 { margin: 1.6em 0 .8em; } +th, td { padding: .8em; } +caption { padding-bottom: .8em; } /* padding instead of margin for IE */ +blockquote { padding: 0 1em; margin: 1.6em 0; } +fieldset { padding: 0 1em 1em 1em; margin: 1.6em 0; } /* padding-top is margin-top for fieldsets in Opera */ +legend { padding-left: .8em; padding-right: .8em; } +legend+* { margin-top: 1em; } /* compensates for the opera margin bug */ +textarea, input { padding: .3em .4em .15em .4em; } +select { padding: .1em .2em 0 .2em; } +option { padding: 0 .4em; } +a { position: relative; padding: 0.3em 0 .1em 0; } /* for larger click-area */ +dt { margin-top: .8em; margin-bottom: .4em; } +ul { margin-left: 1.5em; } +ol { margin-left: 2.35em; } +ol ol, ul ol { margin-left: 2.5em; } +ol ol, ol ul, ul ul, ul ol { margin-bottom: 0; } +form div { margin-bottom: .8em; } + +/* COLORS */ + +a:link { text-decoration: underline; color: #36c; } +a:visited { text-decoration: underline; color: #36c; } +a:hover { text-decoration: underline; color: #c33; } +a:active, a:focus { text-decoration: underline; color: #000; } +code, pre { color: #c33; } /* very optional, but still useful. W3C uses about the same colors for codes */ + +/* 24 COLUMN GRID */ + +.container {width:950px;margin:0 auto;} +.showgrid {background:url(../img/grid.png);} +body {margin:1.5em 0;} +div.span-1, div.span-2, div.span-3, div.span-4, div.span-5, div.span-6, div.span-7, div.span-8, div.span-9, div.span-10, div.span-11, div.span-12, div.span-13, div.span-14, div.span-15, div.span-16, div.span-17, div.span-18, div.span-19, div.span-20, div.span-21, div.span-22, div.span-23 {float:left;margin-right:10px;} +div.span-24 {float:left;} +div.last {margin-right:0;} +.span-1 {width:30px;} +.span-2 {width:70px;} +.span-3 {width:110px;} +.span-4 {width:150px;} +.span-5 {width:190px;} +.span-6 {width:230px;} +.span-7 {width:270px;} +.span-8 {width:310px;} +.span-9 {width:350px;} +.span-10 {width:390px;} +.span-11 {width:430px;} +.span-12 {width:470px;} +.span-13 {width:510px;} +.span-14 {width:550px;} +.span-15 {width:590px;} +.span-16 {width:630px;} +.span-17 {width:670px;} +.span-18 {width:710px;} +.span-19 {width:750px;} +.span-20 {width:790px;} +.span-21 {width:830px;} +.span-22 {width:870px;} +.span-23 {width:910px;} +.span-24, div.span-24 {width:950px;} +.suffix-1 {padding-right:40px;} +.suffix-2 {padding-right:80px;} +.suffix-3 {padding-right:120px;} +.suffix-4 {padding-right:160px;} +.suffix-5 {padding-right:200px;} +.suffix-6 {padding-right:240px;} +.suffix-7 {padding-right:280px;} +.suffix-8 {padding-right:320px;} +.suffix-9 {padding-right:360px;} +.suffix-10 {padding-right:400px;} +.suffix-11 {padding-right:440px;} +.suffix-12 {padding-right:480px;} +.suffix-13 {padding-right:520px;} +.suffix-14 {padding-right:560px;} +.suffix-15 {padding-right:600px;} +.suffix-16 {padding-right:640px;} +.suffix-17 {padding-right:680px;} +.suffix-18 {padding-right:720px;} +.suffix-19 {padding-right:760px;} +.suffix-20 {padding-right:800px;} +.suffix-21 {padding-right:840px;} +.suffix-22 {padding-right:880px;} +.suffix-23 {padding-right:920px;} +.prefix-1 {padding-left:40px;} +.prefix-2 {padding-left:80px;} +.prefix-3 {padding-left:120px;} +.prefix-4 {padding-left:160px;} +.prefix-5 {padding-left:200px;} +.prefix-6 {padding-left:240px;} +.prefix-7 {padding-left:280px;} +.prefix-8 {padding-left:320px;} +.prefix-9 {padding-left:360px;} +.prefix-10 {padding-left:400px;} +.prefix-11 {padding-left:440px;} +.prefix-12 {padding-left:480px;} +.prefix-13 {padding-left:520px;} +.prefix-14 {padding-left:560px;} +.prefix-15 {padding-left:600px;} +.prefix-16 {padding-left:640px;} +.prefix-17 {padding-left:680px;} +.prefix-18 {padding-left:720px;} +.prefix-19 {padding-left:760px;} +.prefix-20 {padding-left:800px;} +.prefix-21 {padding-left:840px;} +.prefix-22 {padding-left:880px;} +.prefix-23 {padding-left:920px;} +div.border {padding-right:4px;margin-right:5px;border-right:1px solid #eee;} +div.colborder {padding-right:24px;margin-right:25px;border-right:1px solid #eee;} +.pull-1 {margin-left:-40px;} +.pull-2 {margin-left:-80px;} +.pull-3 {margin-left:-120px;} +.pull-4 {margin-left:-160px;} +.pull-5 {margin-left:-200px;} +.pull-6 {margin-left:-240px;} +.pull-7 {margin-left:-280px;} +.pull-8 {margin-left:-320px;} +.pull-9 {margin-left:-360px;} +.pull-10 {margin-left:-400px;} +.pull-11 {margin-left:-440px;} +.pull-12 {margin-left:-480px;} +.pull-13 {margin-left:-520px;} +.pull-14 {margin-left:-560px;} +.pull-15 {margin-left:-600px;} +.pull-16 {margin-left:-640px;} +.pull-17 {margin-left:-680px;} +.pull-18 {margin-left:-720px;} +.pull-19 {margin-left:-760px;} +.pull-20 {margin-left:-800px;} +.pull-21 {margin-left:-840px;} +.pull-22 {margin-left:-880px;} +.pull-23 {margin-left:-920px;} +.pull-24 {margin-left:-960px;} +.pull-1, .pull-2, .pull-3, .pull-4, .pull-5, .pull-6, .pull-7, .pull-8, .pull-9, .pull-10, .pull-11, .pull-12, .pull-13, .pull-14, .pull-15, .pull-16, .pull-17, .pull-18, .pull-19, .pull-20, .pull-21, .pull-22, .pull-23, .pull-24 {float:left;position:relative;} +.push-1 {margin:0 -40px 1.5em 40px;} +.push-2 {margin:0 -80px 1.5em 80px;} +.push-3 {margin:0 -120px 1.5em 120px;} +.push-4 {margin:0 -160px 1.5em 160px;} +.push-5 {margin:0 -200px 1.5em 200px;} +.push-6 {margin:0 -240px 1.5em 240px;} +.push-7 {margin:0 -280px 1.5em 280px;} +.push-8 {margin:0 -320px 1.5em 320px;} +.push-9 {margin:0 -360px 1.5em 360px;} +.push-10 {margin:0 -400px 1.5em 400px;} +.push-11 {margin:0 -440px 1.5em 440px;} +.push-12 {margin:0 -480px 1.5em 480px;} +.push-13 {margin:0 -520px 1.5em 520px;} +.push-14 {margin:0 -560px 1.5em 560px;} +.push-15 {margin:0 -600px 1.5em 600px;} +.push-16 {margin:0 -640px 1.5em 640px;} +.push-17 {margin:0 -680px 1.5em 680px;} +.push-18 {margin:0 -720px 1.5em 720px;} +.push-19 {margin:0 -760px 1.5em 760px;} +.push-20 {margin:0 -800px 1.5em 800px;} +.push-21 {margin:0 -840px 1.5em 840px;} +.push-22 {margin:0 -880px 1.5em 880px;} +.push-23 {margin:0 -920px 1.5em 920px;} +.push-24 {margin:0 -960px 1.5em 960px;} +.push-1, .push-2, .push-3, .push-4, .push-5, .push-6, .push-7, .push-8, .push-9, .push-10, .push-11, .push-12, .push-13, .push-14, .push-15, .push-16, .push-17, .push-18, .push-19, .push-20, .push-21, .push-22, .push-23, .push-24 {float:right;position:relative;} +hr {background:#ddd;color:#ddd;clear:both;float:none;width:100%;height:.1em;margin:0 0 1.45em;border:none;} +hr.space {background:#fff;color:#fff;} +.clearfix:after, .container:after {content:".";display:block;height:0;clear:both;visibility:hidden;max-height:0;} +.clearfix, .container {display:inline-block;} +* html .clearfix, * html .container {height:1%;} +.clearfix, .container {display:block;} +.clear {clear:both;} + +/* to create serif italic dramatic text, use this class */ +.fancy { + color: #666; + font-family: "Warnock Pro", "Goudy Old Style","Palatino","Book Antiqua", Georgia, serif; + font-style: italic; + font-weight: normal; +} + +/* creates small caps */ +.caps { + font-variant: small-caps; + letter-spacing: 1px; + text-transform: lowercase; + font-size:1.2em; + font-weight:bold; + padding:0 2px; +} + +/* TEXT CLASSES */ + +.small {font-size:.8em;margin-bottom:1.875em;line-height:1.875em;} +.large {font-size:1.2em;line-height:2.5em;margin-bottom:1.25em;} +.hide {display:none;} +.quiet {color:#666;} +.loud {color:#000;} +.highlight {background:#ff0;} +.top {margin-top:0;padding-top:0;} +.bottom {margin-bottom:0;padding-bottom:0;} +.thin {font-weight: lighter;} +.error, .notice, .success {padding:.8em;margin-bottom:1.6em;border:2px solid #ddd;} +.error {background:#FBE3E4;color:#8a1f11;border-color:#FBC2C4;} +.notice {background:#FFF6BF;color:#514721;border-color:#FFD324;} +.success {background:#E6EFC2;color:#264409;border-color:#C6D880;} +.error a {color:#8a1f11; background:none; padding:0; margin:0; } +.notice a {color:#514721; background:none; padding:0; margin:0; } +.success a {color:#264409; background:none; padding:0; margin:0; } +.left { text-align: left;} +.right {text-align: right;} +.center {text-align: center;} + diff --git a/includes/kohana/modules/userguide/media/css/shCore.css b/includes/kohana/modules/userguide/media/css/shCore.css new file mode 100644 index 00000000..61572457 --- /dev/null +++ b/includes/kohana/modules/userguide/media/css/shCore.css @@ -0,0 +1,330 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/ + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/wiki/SyntaxHighlighter:Donate + * + * @version + * 2.1.364 (October 15 2009) + * + * @copyright + * Copyright (C) 2004-2009 Alex Gorbatchev. + * + * @license + * This file is part of SyntaxHighlighter. + * + * SyntaxHighlighter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * SyntaxHighlighter is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with SyntaxHighlighter. If not, see . + */ +.syntaxhighlighter, +.syntaxhighlighter div, +.syntaxhighlighter code, +.syntaxhighlighter table, +.syntaxhighlighter table td, +.syntaxhighlighter table tr, +.syntaxhighlighter table tbody +{ + margin: 0 !important; + padding: 0 !important; + border: 0 !important; + outline: 0 !important; + background: none !important; + text-align: left !important; + float: none !important; + vertical-align: baseline !important; + position: static !important; + left: auto !important; + top: auto !important; + right: auto !important; + bottom: auto !important; + height: auto !important; + width: auto !important; + line-height: 1.1em !important; + font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace !important; + font-weight: normal !important; + font-style: normal !important; + font-size: 1em !important; + min-height: inherit !important; /* For IE8, FF & WebKit */ + min-height: auto !important; /* For IE7 */ +} + +.syntaxhighlighter +{ + width: 99% !important; /* 99% fixes IE8 horizontal scrollbar */ + margin: 1em 0 1em 0 !important; + padding: 1px !important; /* adds a little border on top and bottom */ + position: relative !important; +} + +.syntaxhighlighter .bold +{ + font-weight: bold !important; +} + +.syntaxhighlighter .italic +{ + font-style: italic !important; +} + +.syntaxhighlighter .line +{ +} + +.syntaxhighlighter .no-wrap .line .content +{ + white-space: pre !important; +} + +.syntaxhighlighter .line table +{ + border-collapse: collapse !important; +} + +.syntaxhighlighter .line td +{ + vertical-align: top !important; +} + +.syntaxhighlighter .line .number +{ + width: 3em !important; +} + +.syntaxhighlighter .line .number code +{ + width: 2.7em !important; + padding-right: .3em !important; + text-align: right !important; + display: block !important; +} + +.syntaxhighlighter .line .content +{ + padding-left: .5em !important; +} + +.syntaxhighlighter .line .spaces +{ +} + +/* Disable border and margin on the lines when no gutter option is set */ +.syntaxhighlighter.nogutter .line .content +{ + border-left: none !important; +} + +.syntaxhighlighter .bar +{ + display: none !important; +} + +.syntaxhighlighter .bar.show +{ + display: block !important; +} + +.syntaxhighlighter.collapsed .bar +{ + display: block !important; +} + +/* Adjust some properties when collapsed */ + +.syntaxhighlighter.collapsed .lines +{ + display: none !important; +} + +.syntaxhighlighter .lines.no-wrap +{ + overflow: auto !important; + overflow-y: hidden !important; +} + +/* Styles for the toolbar */ + +.syntaxhighlighter .toolbar +{ + position: absolute !important; + right: 0px !important; + top: 0px !important; + font-size: 1px !important; + padding: 8px 8px 8px 0 !important; /* in px because images don't scale with ems */ +} + +.syntaxhighlighter.collapsed .toolbar +{ + font-size: 80% !important; + padding: .2em 0 .5em .5em !important; + position: static !important; +} + +.syntaxhighlighter .toolbar a.item, +.syntaxhighlighter .toolbar .item +{ + display: block !important; + float: left !important; + margin-left: 8px !important; + background-repeat: no-repeat !important; + overflow: hidden !important; + text-indent: -5000px !important; +} + +.syntaxhighlighter.collapsed .toolbar .item +{ + display: none !important; +} + +.syntaxhighlighter.collapsed .toolbar .item.expandSource +{ + background-image: url(magnifier.png) !important; + display: inline !important; + text-indent: 0 !important; + width: auto !important; + float: none !important; + height: 16px !important; + padding-left: 20px !important; +} + +.syntaxhighlighter .toolbar .item.viewSource +{ + background-image: url(page_white_code.png) !important; +} + +.syntaxhighlighter .toolbar .item.printSource +{ + background-image: url(printer.png) !important; +} + +.syntaxhighlighter .toolbar .item.copyToClipboard +{ + text-indent: 0 !important; + background: none !important; + overflow: visible !important; +} + +.syntaxhighlighter .toolbar .item.about +{ + background-image: url(help.png) !important; +} + +/** + * Print view. + * Colors are based on the default theme without background. + */ + +.syntaxhighlighter.printing, +.syntaxhighlighter.printing .line.alt1 .content, +.syntaxhighlighter.printing .line.alt2 .content, +.syntaxhighlighter.printing .line.highlighted .number, +.syntaxhighlighter.printing .line.highlighted.alt1 .content, +.syntaxhighlighter.printing .line.highlighted.alt2 .content +{ + background: none !important; +} + +/* Gutter line numbers */ +.syntaxhighlighter.printing .line .number +{ + color: #bbb !important; +} + +/* Add border to the lines */ +.syntaxhighlighter.printing .line .content +{ + color: #000 !important; +} + +/* Toolbar when visible */ +.syntaxhighlighter.printing .toolbar +{ + display: none !important; +} + +.syntaxhighlighter.printing a +{ + text-decoration: none !important; +} + +.syntaxhighlighter.printing .plain, +.syntaxhighlighter.printing .plain a +{ + color: #000 !important; +} + +.syntaxhighlighter.printing .comments, +.syntaxhighlighter.printing .comments a +{ + color: #008200 !important; +} + +.syntaxhighlighter.printing .string, +.syntaxhighlighter.printing .string a +{ + color: blue !important; +} + +.syntaxhighlighter.printing .keyword +{ + color: #069 !important; + font-weight: bold !important; +} + +.syntaxhighlighter.printing .preprocessor +{ + color: gray !important; +} + +.syntaxhighlighter.printing .variable +{ + color: #a70 !important; +} + +.syntaxhighlighter.printing .value +{ + color: #090 !important; +} + +.syntaxhighlighter.printing .functions +{ + color: #ff1493 !important; +} + +.syntaxhighlighter.printing .constants +{ + color: #0066CC !important; +} + +.syntaxhighlighter.printing .script +{ + font-weight: bold !important; +} + +.syntaxhighlighter.printing .color1, +.syntaxhighlighter.printing .color1 a +{ + color: #808080 !important; +} + +.syntaxhighlighter.printing .color2, +.syntaxhighlighter.printing .color2 a +{ + color: #ff1493 !important; +} + +.syntaxhighlighter.printing .color3, +.syntaxhighlighter.printing .color3 a +{ + color: red !important; +} diff --git a/includes/kohana/modules/userguide/media/css/shThemeDefault.css b/includes/kohana/modules/userguide/media/css/shThemeDefault.css new file mode 100644 index 00000000..3fef10d2 --- /dev/null +++ b/includes/kohana/modules/userguide/media/css/shThemeDefault.css @@ -0,0 +1,173 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/ + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/wiki/SyntaxHighlighter:Donate + * + * @version + * 2.1.364 (October 15 2009) + * + * @copyright + * Copyright (C) 2004-2009 Alex Gorbatchev. + * + * @license + * This file is part of SyntaxHighlighter. + * + * SyntaxHighlighter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * SyntaxHighlighter is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with SyntaxHighlighter. If not, see . + */ +/************************************ + * Default Syntax Highlighter theme. + * + * Interface elements. + ************************************/ + +.syntaxhighlighter +{ + background-color: #fff !important; +} + +/* Highlighed line number */ +.syntaxhighlighter .line.highlighted .number +{ + color: black !important; +} + +/* Highlighed line */ +.syntaxhighlighter .line.highlighted.alt1, +.syntaxhighlighter .line.highlighted.alt2 +{ + background-color: #e0e0e0 !important; +} + +/* Gutter line numbers */ +.syntaxhighlighter .line .number +{ + color: #afafaf !important; +} + +/* Add border to the lines */ +.syntaxhighlighter .line .content +{ + border-left: 3px solid #6CE26C !important; + color: #000 !important; +} + +.syntaxhighlighter.printing .line .content +{ + border: 0 !important; +} + +/* First line */ +.syntaxhighlighter .line.alt1 +{ + background-color: #fff !important; +} + +/* Second line */ +.syntaxhighlighter .line.alt2 +{ + background-color: #F8F8F8 !important; +} + +.syntaxhighlighter .toolbar +{ + background-color: #F8F8F8 !important; + border: #E7E5DC solid 1px !important; +} + +.syntaxhighlighter .toolbar a +{ + color: #a0a0a0 !important; +} + +.syntaxhighlighter .toolbar a:hover +{ + color: red !important; +} + +/************************************ + * Actual syntax highlighter colors. + ************************************/ +.syntaxhighlighter .plain, +.syntaxhighlighter .plain a +{ + color: #000 !important; +} + +.syntaxhighlighter .comments, +.syntaxhighlighter .comments a +{ + color: #008200 !important; +} + +.syntaxhighlighter .string, +.syntaxhighlighter .string a +{ + color: blue !important; +} + +.syntaxhighlighter .keyword +{ + color: #069 !important; + font-weight: bold !important; +} + +.syntaxhighlighter .preprocessor +{ + color: gray !important; +} + +.syntaxhighlighter .variable +{ + color: #a70 !important; +} + +.syntaxhighlighter .value +{ + color: #090 !important; +} + +.syntaxhighlighter .functions +{ + color: #ff1493 !important; +} + +.syntaxhighlighter .constants +{ + color: #0066CC !important; +} + +.syntaxhighlighter .script +{ + background-color: yellow !important; +} + +.syntaxhighlighter .color1, +.syntaxhighlighter .color1 a +{ + color: #808080 !important; +} + +.syntaxhighlighter .color2, +.syntaxhighlighter .color2 a +{ + color: #ff1493 !important; +} + +.syntaxhighlighter .color3, +.syntaxhighlighter .color3 a +{ + color: red !important; +} diff --git a/includes/kohana/modules/userguide/media/css/shThemeKodoc.css b/includes/kohana/modules/userguide/media/css/shThemeKodoc.css new file mode 100644 index 00000000..ec4740d1 --- /dev/null +++ b/includes/kohana/modules/userguide/media/css/shThemeKodoc.css @@ -0,0 +1,183 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/ + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/wiki/SyntaxHighlighter:Donate + * + * @version + * 2.1.364 (October 15 2009) + * + * @copyright + * Copyright (C) 2004-2009 Alex Gorbatchev. + * + * @license + * This file is part of SyntaxHighlighter. + * + * SyntaxHighlighter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * SyntaxHighlighter is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with SyntaxHighlighter. If not, see . + */ +/************************************ + * Default Syntax Highlighter theme. + * + * Interface elements. + ************************************/ + +.syntaxhighlighter +{ + +} + +.syntaxhighlighter +{ + width: 100% !important; + margin: 0 !important; + padding: 1px !important; /* adds a little border on top and bottom */ + position: relative !important; + /*background-color: #fff !important;*/ +} + +/* Highlighed line number */ +.syntaxhighlighter .line.highlighted .number +{ + color: black !important; +} + +/* Highlighed line */ +.syntaxhighlighter .line.highlighted.alt1, +.syntaxhighlighter .line.highlighted.alt2 +{ + background-color: #e0e0e0 !important; +} + +/* Gutter line numbers */ +.syntaxhighlighter .line .number +{ + color: #afafaf !important; +} + +/* Add border to the lines */ +.syntaxhighlighter .line .content +{ + border-left: none !important; + padding-left: 0 !important; + color: #000 !important; +} + +.syntaxhighlighter.printing .line .content +{ + border: 0 !important; +} + +/* First line */ +.syntaxhighlighter .line.alt1 +{ + /*background-color: #fff !important;*/ +} + +/* Second line */ +.syntaxhighlighter .line.alt2 +{ + /*background-color: #F8F8F8 !important;*/ +} + +.syntaxhighlighter .toolbar +{ + background-color: #F8F8F8 !important; + border: #E7E5DC solid 1px !important; +} + +.syntaxhighlighter .toolbar a +{ + color: #a0a0a0 !important; +} + +.syntaxhighlighter .toolbar a:hover +{ + color: red !important; +} + +/************************************ + * Actual syntax highlighter colors. + ************************************/ +.syntaxhighlighter .plain, +.syntaxhighlighter .plain a +{ + color: #000 !important; +} + +.syntaxhighlighter .comments, +.syntaxhighlighter .comments a +{ + color: #008200 !important; +} + +.syntaxhighlighter .string, +.syntaxhighlighter .string a +{ + color: blue !important; +} + +.syntaxhighlighter .keyword +{ + color: #069 !important; + font-weight: bold !important; +} + +.syntaxhighlighter .preprocessor +{ + color: gray !important; +} + +.syntaxhighlighter .variable +{ + color: #a70 !important; +} + +.syntaxhighlighter .value +{ + color: #090 !important; +} + +.syntaxhighlighter .functions +{ + color: #ff1493 !important; +} + +.syntaxhighlighter .constants +{ + color: #0066CC !important; +} + +.syntaxhighlighter .script +{ + background-color: yellow !important; +} + +.syntaxhighlighter .color1, +.syntaxhighlighter .color1 a +{ + color: #808080 !important; +} + +.syntaxhighlighter .color2, +.syntaxhighlighter .color2 a +{ + color: #ff1493 !important; +} + +.syntaxhighlighter .color3, +.syntaxhighlighter .color3 a +{ + color: red !important; +} diff --git a/includes/kohana/modules/userguide/media/img/cascading_filesystem.png b/includes/kohana/modules/userguide/media/img/cascading_filesystem.png new file mode 100644 index 0000000000000000000000000000000000000000..d450f7b9922365de5a67b7f617b0ac7d213f8aeb GIT binary patch literal 59970 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

            #wxqXEn^I3-RGhZYFO5-VBWp3m- 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 Z|1qZK%K-Kw@-I$N=xQ2jRH;3E{XfE6!59Dl literal 0 HcmV?d00001 diff --git a/includes/kohana/modules/userguide/media/img/hello_world_1.png b/includes/kohana/modules/userguide/media/img/hello_world_1.png new file mode 100644 index 0000000000000000000000000000000000000000..e4ea75d3a230bc6bbb07c96023342dfb3d9c7613 GIT binary patch literal 1423 zcmV;A1#tR_P)X0ssI2W_dl1000G6Nkl(ewQMW|LhJoM% zRHEQ8IB?1sh(@AD!SN6@ppmzNfQl6g?d|KfySFVZh14s`5|-Ta7hm&n9>4E==bZb2 zmXui)5-16j1WE!Wg#d2Di+Yb6SX9)x7vUWHM0olVd&rz}{UiBZ1Ba zP_6i4*gOEQ!;h6D&{+XGR~$%B51Q%+(p|PM!i&Y? zxpG*pL9yAjuh6#xD(@}-K57Nmg}<2R!t>oCs_It|$BGV|3ttGhSlhFO6|ZqUt?BL* zerhwKGa?FLPyv7zlKDvSCM&9*oE3lx9@?Ff#5F7{{O@@mmyb+$6&2TJ#RwP_09#f- ze11)Suo+OKX)hNRL|z%4Ne!j@hsEbyZEkHSP4Te;`i^TO`uZ{O$JP(PDf(=e9NRhi zkI0Wo4LIB;Cj_bFu)nN)i4rr_h%2J)fs@pzFImdRhr9q_ zNk*GOZ>QxupRj^{HIWsSRzQb{}7F1CHU@4X;$F zwDV%pT2EUupvsP4mr`g!o|Hw?JH67~s%~*_zU*6jcBj`|Z zs5N;0gk5U&)4X;5X_pTPun=>5R9aV8`CV=Hlr;^%fcm;!>VPWeFLMxTAnqmO=G3Y3t7etA#s8h;$LmdJ{EXqJJ>j7lsv zCGFVtFW76&J=eS+P-BL2_ckHZ)%=$kino-bet0`+D2`Zx&3~Bk18!g)R@9gm>Z30O zL{=;UCC<>ksTnG*OIo+ReE9u=YLR=Xe6U(uG>95^zpPH(o?Q6?C8(kIQuqKXvnN%z z!+3QQ5!X;tBLIq*Fvxq??QyN)vQT}5zRUL*nia7XVxW3j-YU{d2#7l!$q?o ziiZb10D!*px?Z8QHh~Fd`AA@e5U8okp9oz5w9UEABWexuq++i>1I)gZo)w182Uwc^ zKu!#Fq;e}?_vs4rY3^|~;-37CF7|+nopWHueckByr2L0yfeVx4%I9)D*C(86k!Va` z!~nK*_qfX2X3Lr}{e*b=K%{_;4X`{GXQeQsvQIGym&gMggq5!?;M9*ti^1XZs);RQ zH?Z5xh`ljp+G5#?d1#Jw?x*Jsw+v~S?kXy-I}#(HQ-ET($UFMrh3r58pgOv2IDzLr zI$Racw`PQ&>`~6u*CvgmHi>WLmRvthKHtrT$eQ;r<3{ARNy+!nSrUy>Do4o&W>QF? dB+&n({0jgausP9y310vJ002ovPDHLkV1nX1sLucZ literal 0 HcmV?d00001 diff --git a/includes/kohana/modules/userguide/media/img/hello_world_2.png b/includes/kohana/modules/userguide/media/img/hello_world_2.png new file mode 100644 index 0000000000000000000000000000000000000000..9eb10f5676d980b0c689a531e822a542b6608224 GIT binary patch literal 6681 zcma)hWl$VU)9n)6-JQkBLI@BbK#;{cIPqVVg0!Tz&(e{R zw;{>EvPs!=G{&@sI*Eov3p7QOCnDGr9`uZ2ceYYnH>?MOoTRGN0kjnNEAX|3_nL9K**XZr+_P3qfWKZq6TN zn6J+Eh?#a19t(bqCZ{1ty0AQyA?=La&?$ZUzpG$Zk}mY^;ae^>g08FO$<+jrCK(wo zB5&6Wb!xfD;zis$l5aKCV^W>PS7edxhKnDLaGeaF6DwNp>Q{Ua!z79+QN7KU6AZd~`bCKR`a+ZmJ*~dFLQ{eS7VsEpz#}g%x2k{SU<%|SZZ>^+Jy>QC-d)8Lv z`fCFvn0RC2V_Oef`s}B-^D4T;EBAU7_7YK43Og(W)yIe0?uZjH@3tRhUre%yR@bJd zyFU*WaJTmy?-%&6l%D4Ufk3I%9LpM+!!i!09*ez0Vj3!;L3vq*EXA@N-E4!sDutl3 z@BT9p=VYwPbc^_cMst(iS4ufmWOOkpN9W1mx%eW4r!aSj`!hD1Pw^$)t&SI|tH)W? zbp%2R9>jWhguU-h);b(Fg}Hd&^0?T#g4n>viz&V4*E-r`K@S!TixT#JRc-n_aHWv43n9KVHGVEds6ntZ z4JX}naNpxNbB(Pqs6r7l{B?|igFcJN&deGBloITQI@FE%NmZ?mA@)t22pjl*2Wn?2 zUNmozj)VG2C@^c{BP+A0?1GCgo^WBAz$bd)Nh@e1Gfx;iRLgT8=-I=u2a(zx(|SAf zN^T*X~nV($BkV0+lqgtjKGKRX=~^oE+blpeSmk@@%D= zpdUEERq1s?A@IDrbyAwQnCG|-afFC^>ctq?zRhge%-`leu$B_91&nV==Uw=-V5nBI zh%q`x0=Lro28)7FtQ4NM)~;{&KGg*v`N>%EvlA21^^BVcCp^%;-wdi|x+lX$Sme$& zbXk}Qz_lbkC5Xl8A6L4BSCY5!peA!zcv_0$dn%W0&lwKZ^`TXUWAuZDR@(n8XOfTj zD(ZkL#?qwY=y{J4kgWWj#Bq{H+UOz>5mSwGSB}T`g1p8vrUPq=eR}v54vR!VG!fX{0`O^+#0p0ge^ixAh zkTRK8;81)Sa8f4hPb+y}&4-Q)Q=tYFDZ#V)3gQ7CCAwHP?AO-cyxR0xS=d((upOf( zd!n#>Ny;H65PqyBDJsePk--W3ka(+GL7!3VM4Uf2xd$)tPqYuEHg{gWPYWt%Z#YPF zH^^EY=2;96$(LUQq?q_}!lNDz{v6Q#B*iejfcLRlN&OgjNe=@(Z+}%q)LkbCLF%ie8p>zheb6

            (zd`Htx_G4rFnh6)=}3X@uMVbnaDf`!>n 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%0Di!s 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

            rN)fl@t-fk)&7nv#&~lz~ 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 JC2bt=zW`TvM@Rqw literal 0 HcmV?d00001 diff --git a/includes/kohana/modules/userguide/media/img/note.png b/includes/kohana/modules/userguide/media/img/note.png new file mode 100644 index 0000000000000000000000000000000000000000..0814a531c9ef499f50cbef3bf18f7808b420c168 GIT binary patch literal 199 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[^>]*$|^#([\w-]+)$/,Ua=/^.[^:#\[\.,]*$/,Va=/\S/, +Wa=/^(\s|\u00A0)+|(\s|\u00A0)+$/g,Xa=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,P=navigator.userAgent,xa=false,Q=[],L,$=Object.prototype.toString,aa=Object.prototype.hasOwnProperty,ba=Array.prototype.push,R=Array.prototype.slice,ya=Array.prototype.indexOf;c.fn=c.prototype={init:function(a,b){var d,f;if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1;return this}if(a==="body"&&!b){this.context=s;this[0]=s.body;this.selector="body";this.length=1;return this}if(typeof a==="string")if((d=Ta.exec(a))&& +(d[1]||!b))if(d[1]){f=b?b.ownerDocument||b:s;if(a=Xa.exec(a))if(c.isPlainObject(b)){a=[s.createElement(a[1])];c.fn.attr.call(a,b,true)}else a=[f.createElement(a[1])];else{a=sa([d[1]],[f]);a=(a.cacheable?a.fragment.cloneNode(true):a.fragment).childNodes}return c.merge(this,a)}else{if(b=s.getElementById(d[2])){if(b.id!==d[2])return T.find(a);this.length=1;this[0]=b}this.context=s;this.selector=a;return this}else if(!b&&/^\w+$/.test(a)){this.selector=a;this.context=s;a=s.getElementsByTagName(a);return c.merge(this, +a)}else return!b||b.jquery?(b||T).find(a):c(b).find(a);else if(c.isFunction(a))return T.ready(a);if(a.selector!==w){this.selector=a.selector;this.context=a.context}return c.makeArray(a,this)},selector:"",jquery:"1.4.2",length:0,size:function(){return this.length},toArray:function(){return R.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this.slice(a)[0]:this[a]},pushStack:function(a,b,d){var f=c();c.isArray(a)?ba.apply(f,a):c.merge(f,a);f.prevObject=this;f.context=this.context;if(b=== +"find")f.selector=this.selector+(this.selector?" ":"")+d;else if(b)f.selector=this.selector+"."+b+"("+d+")";return f},each:function(a,b){return c.each(this,a,b)},ready:function(a){c.bindReady();if(c.isReady)a.call(s,c);else Q&&Q.push(a);return this},eq:function(a){return a===-1?this.slice(a):this.slice(a,+a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(R.apply(this,arguments),"slice",R.call(arguments).join(","))},map:function(a){return this.pushStack(c.map(this, +function(b,d){return a.call(b,d,b)}))},end:function(){return this.prevObject||c(null)},push:ba,sort:[].sort,splice:[].splice};c.fn.init.prototype=c.fn;c.extend=c.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,f=false,e,j,i,o;if(typeof a==="boolean"){f=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!c.isFunction(a))a={};if(d===b){a=this;--b}for(;b
            a"; +var e=d.getElementsByTagName("*"),j=d.getElementsByTagName("a")[0];if(!(!e||!e.length||!j)){c.support={leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(j.getAttribute("style")),hrefNormalized:j.getAttribute("href")==="/a",opacity:/^0.55$/.test(j.style.opacity),cssFloat:!!j.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:s.createElement("select").appendChild(s.createElement("option")).selected, +parentNode:d.removeChild(d.appendChild(s.createElement("div"))).parentNode===null,deleteExpando:true,checkClone:false,scriptEval:false,noCloneEvent:true,boxModel:null};b.type="text/javascript";try{b.appendChild(s.createTextNode("window."+f+"=1;"))}catch(i){}a.insertBefore(b,a.firstChild);if(A[f]){c.support.scriptEval=true;delete A[f]}try{delete b.test}catch(o){c.support.deleteExpando=false}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function k(){c.support.noCloneEvent= +false;d.detachEvent("onclick",k)});d.cloneNode(true).fireEvent("onclick")}d=s.createElement("div");d.innerHTML="";a=s.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var k=s.createElement("div");k.style.width=k.style.paddingLeft="1px";s.body.appendChild(k);c.boxModel=c.support.boxModel=k.offsetWidth===2;s.body.removeChild(k).style.display="none"});a=function(k){var n= +s.createElement("div");k="on"+k;var r=k in n;if(!r){n.setAttribute(k,"return;");r=typeof n[k]==="function"}return r};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=e=j=null}})();c.props={"for":"htmlFor","class":"className",readonly:"readOnly",maxlength:"maxLength",cellspacing:"cellSpacing",rowspan:"rowSpan",colspan:"colSpan",tabindex:"tabIndex",usemap:"useMap",frameborder:"frameBorder"};var G="jQuery"+J(),Ya=0,za={};c.extend({cache:{},expando:G,noData:{embed:true,object:true, +applet:true},data:function(a,b,d){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var f=a[G],e=c.cache;if(!f&&typeof b==="string"&&d===w)return null;f||(f=++Ya);if(typeof b==="object"){a[G]=f;e[f]=c.extend(true,{},b)}else if(!e[f]){a[G]=f;e[f]={}}a=e[f];if(d!==w)a[b]=d;return typeof b==="string"?a[b]:a}},removeData:function(a,b){if(!(a.nodeName&&c.noData[a.nodeName.toLowerCase()])){a=a==A?za:a;var d=a[G],f=c.cache,e=f[d];if(b){if(e){delete e[b];c.isEmptyObject(e)&&c.removeData(a)}}else{if(c.support.deleteExpando)delete a[c.expando]; +else a.removeAttribute&&a.removeAttribute(c.expando);delete f[d]}}}});c.fn.extend({data:function(a,b){if(typeof a==="undefined"&&this.length)return c.data(this[0]);else if(typeof a==="object")return this.each(function(){c.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===w){var f=this.triggerHandler("getData"+d[1]+"!",[d[0]]);if(f===w&&this.length)f=c.data(this[0],a);return f===w&&d[1]?this.data(d[0]):f}else return this.trigger("setData"+d[1]+"!",[d[0],b]).each(function(){c.data(this, +a,b)})},removeData:function(a){return this.each(function(){c.removeData(this,a)})}});c.extend({queue:function(a,b,d){if(a){b=(b||"fx")+"queue";var f=c.data(a,b);if(!d)return f||[];if(!f||c.isArray(d))f=c.data(a,b,c.makeArray(d));else f.push(d);return f}},dequeue:function(a,b){b=b||"fx";var d=c.queue(a,b),f=d.shift();if(f==="inprogress")f=d.shift();if(f){b==="fx"&&d.unshift("inprogress");f.call(a,function(){c.dequeue(a,b)})}}});c.fn.extend({queue:function(a,b){if(typeof a!=="string"){b=a;a="fx"}if(b=== +w)return c.queue(this[0],a);return this.each(function(){var d=c.queue(this,a,b);a==="fx"&&d[0]!=="inprogress"&&c.dequeue(this,a)})},dequeue:function(a){return this.each(function(){c.dequeue(this,a)})},delay:function(a,b){a=c.fx?c.fx.speeds[a]||a:a;b=b||"fx";return this.queue(b,function(){var d=this;setTimeout(function(){c.dequeue(d,b)},a)})},clearQueue:function(a){return this.queue(a||"fx",[])}});var Aa=/[\n\t]/g,ca=/\s+/,Za=/\r/g,$a=/href|src|style/,ab=/(button|input)/i,bb=/(button|input|object|select|textarea)/i, +cb=/^(a|area)$/i,Ba=/radio|checkbox/;c.fn.extend({attr:function(a,b){return X(this,a,b,true,c.attr)},removeAttr:function(a){return this.each(function(){c.attr(this,a,"");this.nodeType===1&&this.removeAttribute(a)})},addClass:function(a){if(c.isFunction(a))return this.each(function(n){var r=c(this);r.addClass(a.call(this,n,r.attr("class")))});if(a&&typeof a==="string")for(var b=(a||"").split(ca),d=0,f=this.length;d-1)return true;return false},val:function(a){if(a===w){var b=this[0];if(b){if(c.nodeName(b,"option"))return(b.attributes.value||{}).specified?b.value:b.text;if(c.nodeName(b,"select")){var d=b.selectedIndex,f=[],e=b.options;b=b.type==="select-one";if(d<0)return null;var j=b?d:0;for(d=b?d+1:e.length;j=0;else if(c.nodeName(this,"select")){var u=c.makeArray(r);c("option",this).each(function(){this.selected= +c.inArray(c(this).val(),u)>=0});if(!u.length)this.selectedIndex=-1}else this.value=r}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true},attr:function(a,b,d,f){if(!a||a.nodeType===3||a.nodeType===8)return w;if(f&&b in c.attrFn)return c(a)[b](d);f=a.nodeType!==1||!c.isXMLDoc(a);var e=d!==w;b=f&&c.props[b]||b;if(a.nodeType===1){var j=$a.test(b);if(b in a&&f&&!j){if(e){b==="type"&&ab.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed"); +a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&&b.specified?b.value:bb.test(a.nodeName)||cb.test(a.nodeName)&&a.href?0:w;return a[b]}if(!c.support.style&&f&&b==="style"){if(e)a.style.cssText=""+d;return a.style.cssText}e&&a.setAttribute(b,""+d);a=!c.support.hrefNormalized&&f&&j?a.getAttribute(b,2):a.getAttribute(b);return a===null?w:a}return c.style(a,b,d)}});var O=/\.(.*)$/,db=function(a){return a.replace(/[^\w\s\.\|`]/g, +function(b){return"\\"+b})};c.event={add:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){if(a.setInterval&&a!==A&&!a.frameElement)a=A;var e,j;if(d.handler){e=d;d=e.handler}if(!d.guid)d.guid=c.guid++;if(j=c.data(a)){var i=j.events=j.events||{},o=j.handle;if(!o)j.handle=o=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(o.elem,arguments):w};o.elem=a;b=b.split(" ");for(var k,n=0,r;k=b[n++];){j=e?c.extend({},e):{handler:d,data:f};if(k.indexOf(".")>-1){r=k.split("."); +k=r.shift();j.namespace=r.slice(0).sort().join(".")}else{r=[];j.namespace=""}j.type=k;j.guid=d.guid;var u=i[k],z=c.event.special[k]||{};if(!u){u=i[k]=[];if(!z.setup||z.setup.call(a,f,r,o)===false)if(a.addEventListener)a.addEventListener(k,o,false);else a.attachEvent&&a.attachEvent("on"+k,o)}if(z.add){z.add.call(a,j);if(!j.handler.guid)j.handler.guid=d.guid}u.push(j);c.event.global[k]=true}a=null}}},global:{},remove:function(a,b,d,f){if(!(a.nodeType===3||a.nodeType===8)){var e,j=0,i,o,k,n,r,u,z=c.data(a), +C=z&&z.events;if(z&&C){if(b&&b.type){d=b.handler;b=b.type}if(!b||typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(e in C)c.event.remove(a,e+b)}else{for(b=b.split(" ");e=b[j++];){n=e;i=e.indexOf(".")<0;o=[];if(!i){o=e.split(".");e=o.shift();k=new RegExp("(^|\\.)"+c.map(o.slice(0).sort(),db).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(r=C[e])if(d){n=c.event.special[e]||{};for(B=f||0;B=0){a.type= +e=e.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[e]&&c.each(c.cache,function(){this.events&&this.events[e]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType===8)return w;a.result=w;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(f=c.data(d,"handle"))&&f.apply(d,b);f=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+e]&&d["on"+e].apply(d,b)===false)a.result=false}catch(j){}if(!a.isPropagationStopped()&& +f)c.event.trigger(a,b,f,true);else if(!a.isDefaultPrevented()){f=a.target;var i,o=c.nodeName(f,"a")&&e==="click",k=c.event.special[e]||{};if((!k._default||k._default.call(d,a)===false)&&!o&&!(f&&f.nodeName&&c.noData[f.nodeName.toLowerCase()])){try{if(f[e]){if(i=f["on"+e])f["on"+e]=null;c.event.triggered=true;f[e]()}}catch(n){}if(i)f["on"+e]=i;c.event.triggered=false}}},handle:function(a){var b,d,f,e;a=arguments[0]=c.event.fix(a||A.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive; +if(!b){d=a.type.split(".");a.type=d.shift();f=new RegExp("(^|\\.)"+d.slice(0).sort().join("\\.(?:.*\\.)?")+"(\\.|$)")}e=c.data(this,"events");d=e[a.type];if(e&&d){d=d.slice(0);e=0;for(var j=d.length;e-1?c.map(a.options,function(f){return f.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},fa=function(a,b){var d=a.target,f,e;if(!(!da.test(d.nodeName)||d.readOnly)){f=c.data(d,"_change_data");e=Fa(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data", +e);if(!(f===w||e===f))if(f!=null||e){a.type="change";return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:fa,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return fa.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return fa.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a, +"_change_data",Fa(a))}},setup:function(){if(this.type==="file")return false;for(var a in ea)c.event.add(this,a+".specialChange",ea[a]);return da.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return da.test(this.nodeName)}};ea=c.event.special.change.filters}s.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(f){f=c.event.fix(f);f.type=b;return c.event.handle.call(this,f)}c.event.special[b]={setup:function(){this.addEventListener(a, +d,true)},teardown:function(){this.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,f,e){if(typeof d==="object"){for(var j in d)this[b](j,f,d[j],e);return this}if(c.isFunction(f)){e=f;f=w}var i=b==="one"?c.proxy(e,function(k){c(this).unbind(k,i);return e.apply(this,arguments)}):e;if(d==="unload"&&b!=="one")this.one(d,f,e);else{j=0;for(var o=this.length;j0){y=t;break}}t=t[g]}m[q]=y}}}var f=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^[\]]*\]|['"][^'"]*['"]|[^[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g, +e=0,j=Object.prototype.toString,i=false,o=true;[0,0].sort(function(){o=false;return 0});var k=function(g,h,l,m){l=l||[];var q=h=h||s;if(h.nodeType!==1&&h.nodeType!==9)return[];if(!g||typeof g!=="string")return l;for(var p=[],v,t,y,S,H=true,M=x(h),I=g;(f.exec(""),v=f.exec(I))!==null;){I=v[3];p.push(v[1]);if(v[2]){S=v[3];break}}if(p.length>1&&r.exec(g))if(p.length===2&&n.relative[p[0]])t=ga(p[0]+p[1],h);else for(t=n.relative[p[0]]?[h]:k(p.shift(),h);p.length;){g=p.shift();if(n.relative[g])g+=p.shift(); +t=ga(g,t)}else{if(!m&&p.length>1&&h.nodeType===9&&!M&&n.match.ID.test(p[0])&&!n.match.ID.test(p[p.length-1])){v=k.find(p.shift(),h,M);h=v.expr?k.filter(v.expr,v.set)[0]:v.set[0]}if(h){v=m?{expr:p.pop(),set:z(m)}:k.find(p.pop(),p.length===1&&(p[0]==="~"||p[0]==="+")&&h.parentNode?h.parentNode:h,M);t=v.expr?k.filter(v.expr,v.set):v.set;if(p.length>0)y=z(t);else H=false;for(;p.length;){var D=p.pop();v=D;if(n.relative[D])v=p.pop();else D="";if(v==null)v=h;n.relative[D](y,v,M)}}else y=[]}y||(y=t);y||k.error(D|| +g);if(j.call(y)==="[object Array]")if(H)if(h&&h.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&E(h,y[g])))l.push(t[g])}else for(g=0;y[g]!=null;g++)y[g]&&y[g].nodeType===1&&l.push(t[g]);else l.push.apply(l,y);else z(y,l);if(S){k(S,q,l,m);k.uniqueSort(l)}return l};k.uniqueSort=function(g){if(B){i=o;g.sort(B);if(i)for(var h=1;h":function(g,h){var l=typeof h==="string";if(l&&!/\W/.test(h)){h=h.toLowerCase();for(var m=0,q=g.length;m=0))l||m.push(v);else if(l)h[p]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()}, +CHILD:function(g){if(g[1]==="nth"){var h=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=h[1]+(h[2]||1)-0;g[3]=h[3]-0}g[0]=e++;return g},ATTR:function(g,h,l,m,q,p){h=g[1].replace(/\\/g,"");if(!p&&n.attrMap[h])g[1]=n.attrMap[h];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,h,l,m,q){if(g[1]==="not")if((f.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=k(g[3],null,null,h);else{g=k.filter(g[3],h,l,true^q);l||m.push.apply(m, +g);return false}else if(n.match.POS.test(g[0])||n.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled===true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,h,l){return!!k(l[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)}, +text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"===g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}}, +setFilters:{first:function(g,h){return h===0},last:function(g,h,l,m){return h===m.length-1},even:function(g,h){return h%2===0},odd:function(g,h){return h%2===1},lt:function(g,h,l){return hl[3]-0},nth:function(g,h,l){return l[3]-0===h},eq:function(g,h,l){return l[3]-0===h}},filter:{PSEUDO:function(g,h,l,m){var q=h[1],p=n.filters[q];if(p)return p(g,l,h,m);else if(q==="contains")return(g.textContent||g.innerText||a([g])||"").indexOf(h[3])>=0;else if(q==="not"){h= +h[3];l=0;for(m=h.length;l=0}},ID:function(g,h){return g.nodeType===1&&g.getAttribute("id")===h},TAG:function(g,h){return h==="*"&&g.nodeType===1||g.nodeName.toLowerCase()===h},CLASS:function(g,h){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(h)>-1},ATTR:function(g,h){var l=h[1];g=n.attrHandle[l]?n.attrHandle[l](g):g[l]!=null?g[l]:g.getAttribute(l);l=g+"";var m=h[2];h=h[4];return g==null?m==="!=":m=== +"="?l===h:m==="*="?l.indexOf(h)>=0:m==="~="?(" "+l+" ").indexOf(h)>=0:!h?l&&g!==false:m==="!="?l!==h:m==="^="?l.indexOf(h)===0:m==="$="?l.substr(l.length-h.length)===h:m==="|="?l===h||l.substr(0,h.length+1)===h+"-":false},POS:function(g,h,l,m){var q=n.setFilters[h[2]];if(q)return q(g,l,h,m)}}},r=n.match.POS;for(var u in n.match){n.match[u]=new RegExp(n.match[u].source+/(?![^\[]*\])(?![^\(]*\))/.source);n.leftMatch[u]=new RegExp(/(^(?:.|\r|\n)*?)/.source+n.match[u].source.replace(/\\(\d+)/g,function(g, +h){return"\\"+(h-0+1)}))}var z=function(g,h){g=Array.prototype.slice.call(g,0);if(h){h.push.apply(h,g);return h}return g};try{Array.prototype.slice.call(s.documentElement.childNodes,0)}catch(C){z=function(g,h){h=h||[];if(j.call(g)==="[object Array]")Array.prototype.push.apply(h,g);else if(typeof g.length==="number")for(var l=0,m=g.length;l";var l=s.documentElement;l.insertBefore(g,l.firstChild);if(s.getElementById(h)){n.find.ID=function(m,q,p){if(typeof q.getElementById!=="undefined"&&!p)return(q=q.getElementById(m[1]))?q.id===m[1]||typeof q.getAttributeNode!=="undefined"&& +q.getAttributeNode("id").nodeValue===m[1]?[q]:w:[]};n.filter.ID=function(m,q){var p=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&p&&p.nodeValue===q}}l.removeChild(g);l=g=null})();(function(){var g=s.createElement("div");g.appendChild(s.createComment(""));if(g.getElementsByTagName("*").length>0)n.find.TAG=function(h,l){l=l.getElementsByTagName(h[1]);if(h[1]==="*"){h=[];for(var m=0;l[m];m++)l[m].nodeType===1&&h.push(l[m]);l=h}return l};g.innerHTML=""; +if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")n.attrHandle.href=function(h){return h.getAttribute("href",2)};g=null})();s.querySelectorAll&&function(){var g=k,h=s.createElement("div");h.innerHTML="

            ";if(!(h.querySelectorAll&&h.querySelectorAll(".TEST").length===0)){k=function(m,q,p,v){q=q||s;if(!v&&q.nodeType===9&&!x(q))try{return z(q.querySelectorAll(m),p)}catch(t){}return g(m,q,p,v)};for(var l in g)k[l]=g[l];h=null}}(); +(function(){var g=s.createElement("div");g.innerHTML="
            ";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){n.order.splice(1,0,"CLASS");n.find.CLASS=function(h,l,m){if(typeof l.getElementsByClassName!=="undefined"&&!m)return l.getElementsByClassName(h[1])};g=null}}})();var E=s.compareDocumentPosition?function(g,h){return!!(g.compareDocumentPosition(h)&16)}: +function(g,h){return g!==h&&(g.contains?g.contains(h):true)},x=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false},ga=function(g,h){var l=[],m="",q;for(h=h.nodeType?[h]:h;q=n.match.PSEUDO.exec(g);){m+=q[0];g=g.replace(n.match.PSEUDO,"")}g=n.relative[g]?g+"*":g;q=0;for(var p=h.length;q=0===d})};c.fn.extend({find:function(a){for(var b=this.pushStack("","find",a),d=0,f=0,e=this.length;f0)for(var j=d;j0},closest:function(a,b){if(c.isArray(a)){var d=[],f=this[0],e,j= +{},i;if(f&&a.length){e=0;for(var o=a.length;e-1:c(f).is(e)){d.push({selector:i,elem:f});delete j[i]}}f=f.parentNode}}return d}var k=c.expr.match.POS.test(a)?c(a,b||this.context):null;return this.map(function(n,r){for(;r&&r.ownerDocument&&r!==b;){if(k?k.index(r)>-1:c(r).is(a))return r;r=r.parentNode}return null})},index:function(a){if(!a||typeof a=== +"string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){a=typeof a==="string"?c(a,b||this.context):c.makeArray(a);b=c.merge(this.get(),a);return this.pushStack(qa(a[0])||qa(b[0])?b:c.unique(b))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode", +d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a,2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")? +a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a,b){c.fn[a]=function(d,f){var e=c.map(this,b,d);eb.test(a)||(f=d);if(f&&typeof f==="string")e=c.filter(f,e);e=this.length>1?c.unique(e):e;if((this.length>1||gb.test(f))&&fb.test(a))e=e.reverse();return this.pushStack(e,a,R.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return c.find.matches(a,b)},dir:function(a,b,d){var f=[];for(a=a[b];a&&a.nodeType!==9&&(d===w||a.nodeType!==1||!c(a).is(d));){a.nodeType=== +1&&f.push(a);a=a[b]}return f},nth:function(a,b,d){b=b||1;for(var f=0;a;a=a[d])if(a.nodeType===1&&++f===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var Ja=/ jQuery\d+="(?:\d+|null)"/g,V=/^\s+/,Ka=/(<([\w:]+)[^>]*?)\/>/g,hb=/^(?:area|br|col|embed|hr|img|input|link|meta|param)$/i,La=/<([\w:]+)/,ib=/"},F={option:[1,""],legend:[1,"
            ","
            "],thead:[1,"","
            "],tr:[2,"","
            "],td:[3,"","
            "],col:[2,"","
            "],area:[1,"",""],_default:[0,"",""]};F.optgroup=F.option;F.tbody=F.tfoot=F.colgroup=F.caption=F.thead;F.th=F.td;if(!c.support.htmlSerialize)F._default=[1,"div
            ","
            "];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d= +c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==w)return this.empty().append((this[0]&&this[0].ownerDocument||s).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this}, +wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})}, +prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b, +this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,f;(f=this[d])!=null;d++)if(!a||c.filter(a,[f]).length){if(!b&&f.nodeType===1){c.cleanData(f.getElementsByTagName("*"));c.cleanData([f])}f.parentNode&&f.parentNode.removeChild(f)}return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild); +return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,f=this.ownerDocument;if(!d){d=f.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(Ja,"").replace(/=([^="'>\s]+\/)>/g,'="$1">').replace(V,"")],f)[0]}else return this.cloneNode(true)});if(a===true){ra(this,b);ra(this.find("*"),b.find("*"))}return b},html:function(a){if(a===w)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(Ja, +""):null;else if(typeof a==="string"&&!ta.test(a)&&(c.support.leadingWhitespace||!V.test(a))&&!F[(La.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Ka,Ma);try{for(var b=0,d=this.length;b0||e.cacheable||this.length>1?k.cloneNode(true):k)}o.length&&c.each(o,Qa)}return this}});c.fragments={};c.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var f=[];d=c(d);var e=this.length===1&&this[0].parentNode;if(e&&e.nodeType===11&&e.childNodes.length===1&&d.length===1){d[b](this[0]); +return this}else{e=0;for(var j=d.length;e0?this.clone(true):this).get();c.fn[b].apply(c(d[e]),i);f=f.concat(i)}return this.pushStack(f,a,d.selector)}}});c.extend({clean:function(a,b,d,f){b=b||s;if(typeof b.createElement==="undefined")b=b.ownerDocument||b[0]&&b[0].ownerDocument||s;for(var e=[],j=0,i;(i=a[j])!=null;j++){if(typeof i==="number")i+="";if(i){if(typeof i==="string"&&!jb.test(i))i=b.createTextNode(i);else if(typeof i==="string"){i=i.replace(Ka,Ma);var o=(La.exec(i)||["", +""])[1].toLowerCase(),k=F[o]||F._default,n=k[0],r=b.createElement("div");for(r.innerHTML=k[1]+i+k[2];n--;)r=r.lastChild;if(!c.support.tbody){n=ib.test(i);o=o==="table"&&!n?r.firstChild&&r.firstChild.childNodes:k[1]===""&&!n?r.childNodes:[];for(k=o.length-1;k>=0;--k)c.nodeName(o[k],"tbody")&&!o[k].childNodes.length&&o[k].parentNode.removeChild(o[k])}!c.support.leadingWhitespace&&V.test(i)&&r.insertBefore(b.createTextNode(V.exec(i)[0]),r.firstChild);i=r.childNodes}if(i.nodeType)e.push(i);else e= +c.merge(e,i)}}if(d)for(j=0;e[j];j++)if(f&&c.nodeName(e[j],"script")&&(!e[j].type||e[j].type.toLowerCase()==="text/javascript"))f.push(e[j].parentNode?e[j].parentNode.removeChild(e[j]):e[j]);else{e[j].nodeType===1&&e.splice.apply(e,[j+1,0].concat(c.makeArray(e[j].getElementsByTagName("script"))));d.appendChild(e[j])}return e},cleanData:function(a){for(var b,d,f=c.cache,e=c.event.special,j=c.support.deleteExpando,i=0,o;(o=a[i])!=null;i++)if(d=o[c.expando]){b=f[d];if(b.events)for(var k in b.events)e[k]? +c.event.remove(o,k):Ca(o,k,b.handle);if(j)delete o[c.expando];else o.removeAttribute&&o.removeAttribute(c.expando);delete f[d]}}});var kb=/z-?index|font-?weight|opacity|zoom|line-?height/i,Na=/alpha\([^)]*\)/,Oa=/opacity=([^)]*)/,ha=/float/i,ia=/-([a-z])/ig,lb=/([A-Z])/g,mb=/^-?\d+(?:px)?$/i,nb=/^-?\d/,ob={position:"absolute",visibility:"hidden",display:"block"},pb=["Left","Right"],qb=["Top","Bottom"],rb=s.defaultView&&s.defaultView.getComputedStyle,Pa=c.support.cssFloat?"cssFloat":"styleFloat",ja= +function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){return X(this,a,b,true,function(d,f,e){if(e===w)return c.curCSS(d,f);if(typeof e==="number"&&!kb.test(f))e+="px";c.style(d,f,e)})};c.extend({style:function(a,b,d){if(!a||a.nodeType===3||a.nodeType===8)return w;if((b==="width"||b==="height")&&parseFloat(d)<0)d=w;var f=a.style||a,e=d!==w;if(!c.support.opacity&&b==="opacity"){if(e){f.zoom=1;b=parseInt(d,10)+""==="NaN"?"":"alpha(opacity="+d*100+")";a=f.filter||c.curCSS(a,"filter")||"";f.filter= +Na.test(a)?a.replace(Na,b):b}return f.filter&&f.filter.indexOf("opacity=")>=0?parseFloat(Oa.exec(f.filter)[1])/100+"":""}if(ha.test(b))b=Pa;b=b.replace(ia,ja);if(e)f[b]=d;return f[b]},css:function(a,b,d,f){if(b==="width"||b==="height"){var e,j=b==="width"?pb:qb;function i(){e=b==="width"?a.offsetWidth:a.offsetHeight;f!=="border"&&c.each(j,function(){f||(e-=parseFloat(c.curCSS(a,"padding"+this,true))||0);if(f==="margin")e+=parseFloat(c.curCSS(a,"margin"+this,true))||0;else e-=parseFloat(c.curCSS(a, +"border"+this+"Width",true))||0})}a.offsetWidth!==0?i():c.swap(a,ob,i);return Math.max(0,Math.round(e))}return c.curCSS(a,b,d)},curCSS:function(a,b,d){var f,e=a.style;if(!c.support.opacity&&b==="opacity"&&a.currentStyle){f=Oa.test(a.currentStyle.filter||"")?parseFloat(RegExp.$1)/100+"":"";return f===""?"1":f}if(ha.test(b))b=Pa;if(!d&&e&&e[b])f=e[b];else if(rb){if(ha.test(b))b="float";b=b.replace(lb,"-$1").toLowerCase();e=a.ownerDocument.defaultView;if(!e)return null;if(a=e.getComputedStyle(a,null))f= +a.getPropertyValue(b);if(b==="opacity"&&f==="")f="1"}else if(a.currentStyle){d=b.replace(ia,ja);f=a.currentStyle[b]||a.currentStyle[d];if(!mb.test(f)&&nb.test(f)){b=e.left;var j=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;e.left=d==="fontSize"?"1em":f||0;f=e.pixelLeft+"px";e.left=b;a.runtimeStyle.left=j}}return f},swap:function(a,b,d){var f={};for(var e in b){f[e]=a.style[e];a.style[e]=b[e]}d.call(a);for(e in b)a.style[e]=f[e]}});if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b= +a.offsetWidth,d=a.offsetHeight,f=a.nodeName.toLowerCase()==="tr";return b===0&&d===0&&!f?true:b>0&&d>0&&!f?false:c.curCSS(a,"display")==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var sb=J(),tb=//gi,ub=/select|textarea/i,vb=/color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week/i,N=/=\?(&|$)/,ka=/\?/,wb=/(\?|&)_=.*?(&|$)/,xb=/^(\w+:)?\/\/([^\/?#]+)/,yb=/%20/g,zb=c.fn.load;c.fn.extend({load:function(a,b,d){if(typeof a!== +"string")return zb.call(this,a);else if(!this.length)return this;var f=a.indexOf(" ");if(f>=0){var e=a.slice(f,a.length);a=a.slice(0,f)}f="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b==="object"){b=c.param(b,c.ajaxSettings.traditional);f="POST"}var j=this;c.ajax({url:a,type:f,dataType:"html",data:b,complete:function(i,o){if(o==="success"||o==="notmodified")j.html(e?c("
            ").append(i.responseText.replace(tb,"")).find(e):i.responseText);d&&j.each(d,[i.responseText,o,i])}});return this}, +serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||ub.test(this.nodeName)||vb.test(this.type))}).map(function(a,b){a=c(this).val();return a==null?null:c.isArray(a)?c.map(a,function(d){return{name:b.name,value:d}}):{name:b.name,value:a}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "), +function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:f})},getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,f){if(c.isFunction(b)){f=f||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:f})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href, +global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:A.XMLHttpRequest&&(A.location.protocol!=="file:"||!A.ActiveXObject)?function(){return new A.XMLHttpRequest}:function(){try{return new A.ActiveXObject("Microsoft.XMLHTTP")}catch(a){}},accepts:{xml:"application/xml, text/xml",html:"text/html",script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},lastModified:{},etag:{},ajax:function(a){function b(){e.success&& +e.success.call(k,o,i,x);e.global&&f("ajaxSuccess",[x,e])}function d(){e.complete&&e.complete.call(k,x,i);e.global&&f("ajaxComplete",[x,e]);e.global&&!--c.active&&c.event.trigger("ajaxStop")}function f(q,p){(e.context?c(e.context):c.event).trigger(q,p)}var e=c.extend(true,{},c.ajaxSettings,a),j,i,o,k=a&&a.context||e,n=e.type.toUpperCase();if(e.data&&e.processData&&typeof e.data!=="string")e.data=c.param(e.data,e.traditional);if(e.dataType==="jsonp"){if(n==="GET")N.test(e.url)||(e.url+=(ka.test(e.url)? +"&":"?")+(e.jsonp||"callback")+"=?");else if(!e.data||!N.test(e.data))e.data=(e.data?e.data+"&":"")+(e.jsonp||"callback")+"=?";e.dataType="json"}if(e.dataType==="json"&&(e.data&&N.test(e.data)||N.test(e.url))){j=e.jsonpCallback||"jsonp"+sb++;if(e.data)e.data=(e.data+"").replace(N,"="+j+"$1");e.url=e.url.replace(N,"="+j+"$1");e.dataType="script";A[j]=A[j]||function(q){o=q;b();d();A[j]=w;try{delete A[j]}catch(p){}z&&z.removeChild(C)}}if(e.dataType==="script"&&e.cache===null)e.cache=false;if(e.cache=== +false&&n==="GET"){var r=J(),u=e.url.replace(wb,"$1_="+r+"$2");e.url=u+(u===e.url?(ka.test(e.url)?"&":"?")+"_="+r:"")}if(e.data&&n==="GET")e.url+=(ka.test(e.url)?"&":"?")+e.data;e.global&&!c.active++&&c.event.trigger("ajaxStart");r=(r=xb.exec(e.url))&&(r[1]&&r[1]!==location.protocol||r[2]!==location.host);if(e.dataType==="script"&&n==="GET"&&r){var z=s.getElementsByTagName("head")[0]||s.documentElement,C=s.createElement("script");C.src=e.url;if(e.scriptCharset)C.charset=e.scriptCharset;if(!j){var B= +false;C.onload=C.onreadystatechange=function(){if(!B&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){B=true;b();d();C.onload=C.onreadystatechange=null;z&&C.parentNode&&z.removeChild(C)}}}z.insertBefore(C,z.firstChild);return w}var E=false,x=e.xhr();if(x){e.username?x.open(n,e.url,e.async,e.username,e.password):x.open(n,e.url,e.async);try{if(e.data||a&&a.contentType)x.setRequestHeader("Content-Type",e.contentType);if(e.ifModified){c.lastModified[e.url]&&x.setRequestHeader("If-Modified-Since", +c.lastModified[e.url]);c.etag[e.url]&&x.setRequestHeader("If-None-Match",c.etag[e.url])}r||x.setRequestHeader("X-Requested-With","XMLHttpRequest");x.setRequestHeader("Accept",e.dataType&&e.accepts[e.dataType]?e.accepts[e.dataType]+", */*":e.accepts._default)}catch(ga){}if(e.beforeSend&&e.beforeSend.call(k,x,e)===false){e.global&&!--c.active&&c.event.trigger("ajaxStop");x.abort();return false}e.global&&f("ajaxSend",[x,e]);var g=x.onreadystatechange=function(q){if(!x||x.readyState===0||q==="abort"){E|| +d();E=true;if(x)x.onreadystatechange=c.noop}else if(!E&&x&&(x.readyState===4||q==="timeout")){E=true;x.onreadystatechange=c.noop;i=q==="timeout"?"timeout":!c.httpSuccess(x)?"error":e.ifModified&&c.httpNotModified(x,e.url)?"notmodified":"success";var p;if(i==="success")try{o=c.httpData(x,e.dataType,e)}catch(v){i="parsererror";p=v}if(i==="success"||i==="notmodified")j||b();else c.handleError(e,x,i,p);d();q==="timeout"&&x.abort();if(e.async)x=null}};try{var h=x.abort;x.abort=function(){x&&h.call(x); +g("abort")}}catch(l){}e.async&&e.timeout>0&&setTimeout(function(){x&&!E&&g("timeout")},e.timeout);try{x.send(n==="POST"||n==="PUT"||n==="DELETE"?e.data:null)}catch(m){c.handleError(e,x,null,m);d()}e.async||g();return x}},handleError:function(a,b,d,f){if(a.error)a.error.call(a.context||a,b,d,f);if(a.global)(a.context?c(a.context):c.event).trigger("ajaxError",[b,a,f])},active:0,httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status=== +1223||a.status===0}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"),f=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(f)c.etag[b]=f;return a.status===304||a.status===0},httpData:function(a,b,d){var f=a.getResponseHeader("content-type")||"",e=b==="xml"||!b&&f.indexOf("xml")>=0;a=e?a.responseXML:a.responseText;e&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b=== +"json"||!b&&f.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&f.indexOf("javascript")>=0)c.globalEval(a);return a},param:function(a,b){function d(i,o){if(c.isArray(o))c.each(o,function(k,n){b||/\[\]$/.test(i)?f(i,n):d(i+"["+(typeof n==="object"||c.isArray(n)?k:"")+"]",n)});else!b&&o!=null&&typeof o==="object"?c.each(o,function(k,n){d(i+"["+k+"]",n)}):f(i,o)}function f(i,o){o=c.isFunction(o)?o():o;e[e.length]=encodeURIComponent(i)+"="+encodeURIComponent(o)}var e=[];if(b===w)b=c.ajaxSettings.traditional; +if(c.isArray(a)||a.jquery)c.each(a,function(){f(this.name,this.value)});else for(var j in a)d(j,a[j]);return e.join("&").replace(yb,"+")}});var la={},Ab=/toggle|show|hide/,Bb=/^([+-]=)?([\d+-.]+)(.*)$/,W,va=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b){if(a||a===0)return this.animate(K("show",3),a,b);else{a=0;for(b=this.length;a").appendTo("body");f=e.css("display");if(f==="none")f="block";e.remove();la[d]=f}c.data(this[a],"olddisplay",f)}}a=0;for(b=this.length;a=0;f--)if(d[f].elem===this){b&&d[f](true);d.splice(f,1)}});b||this.dequeue();return this}});c.each({slideDown:K("show",1),slideUp:K("hide",1),slideToggle:K("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"}},function(a,b){c.fn[a]=function(d,f){return this.animate(b,d,f)}});c.extend({speed:function(a,b,d){var f=a&&typeof a==="object"?a:{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};f.duration=c.fx.off?0:typeof f.duration=== +"number"?f.duration:c.fx.speeds[f.duration]||c.fx.speeds._default;f.old=f.complete;f.complete=function(){f.queue!==false&&c(this).dequeue();c.isFunction(f.old)&&f.old.call(this)};return f},easing:{linear:function(a,b,d,f){return d+f*a},swing:function(a,b,d,f){return(-Math.cos(a*Math.PI)/2+0.5)*f+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]|| +c.fx.step._default)(this);if((this.prop==="height"||this.prop==="width")&&this.elem.style)this.elem.style.display="block"},cur:function(a){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];return(a=parseFloat(c.css(this.elem,this.prop,a)))&&a>-10000?a:parseFloat(c.curCSS(this.elem,this.prop))||0},custom:function(a,b,d){function f(j){return e.step(j)}this.startTime=J();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start; +this.pos=this.state=0;var e=this;f.elem=this.elem;if(f()&&c.timers.push(f)&&!W)W=setInterval(c.fx.tick,13)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true;this.custom(this.cur(),0)},step:function(a){var b=J(),d=true;if(a||b>=this.options.duration+this.startTime){this.now= +this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var f in this.options.curAnim)if(this.options.curAnim[f]!==true)d=false;if(d){if(this.options.display!=null){this.elem.style.overflow=this.options.overflow;a=c.data(this.elem,"olddisplay");this.elem.style.display=a?a:this.options.display;if(c.css(this.elem,"display")==="none")this.elem.style.display="block"}this.options.hide&&c(this.elem).hide();if(this.options.hide||this.options.show)for(var e in this.options.curAnim)c.style(this.elem, +e,this.options.orig[e]);this.options.complete.call(this.elem)}return false}else{e=b-this.startTime;this.state=e/this.options.duration;a=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||a](this.state,e,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a=c.timers,b=0;b
            "; +a.insertBefore(b,a.firstChild);d=b.firstChild;f=d.firstChild;e=d.nextSibling.firstChild.firstChild;this.doesNotAddBorder=f.offsetTop!==5;this.doesAddBorderForTableAndCells=e.offsetTop===5;f.style.position="fixed";f.style.top="20px";this.supportsFixedPosition=f.offsetTop===20||f.offsetTop===15;f.style.position=f.style.top="";d.style.overflow="hidden";d.style.position="relative";this.subtractsBorderForOverflowNotVisible=f.offsetTop===-5;this.doesNotIncludeMarginInBodyOffset=a.offsetTop!==j;a.removeChild(b); +c.offset.initialize=c.noop},bodyOffset:function(a){var b=a.offsetTop,d=a.offsetLeft;c.offset.initialize();if(c.offset.doesNotIncludeMarginInBodyOffset){b+=parseFloat(c.curCSS(a,"marginTop",true))||0;d+=parseFloat(c.curCSS(a,"marginLeft",true))||0}return{top:b,left:d}},setOffset:function(a,b,d){if(/static/.test(c.curCSS(a,"position")))a.style.position="relative";var f=c(a),e=f.offset(),j=parseInt(c.curCSS(a,"top",true),10)||0,i=parseInt(c.curCSS(a,"left",true),10)||0;if(c.isFunction(b))b=b.call(a, +d,e);d={top:b.top-e.top+j,left:b.left-e.left+i};"using"in b?b.using.call(a,d):f.css(d)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),f=/^body|html$/i.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.curCSS(a,"marginTop",true))||0;d.left-=parseFloat(c.curCSS(a,"marginLeft",true))||0;f.top+=parseFloat(c.curCSS(b[0],"borderTopWidth",true))||0;f.left+=parseFloat(c.curCSS(b[0],"borderLeftWidth",true))||0;return{top:d.top- +f.top,left:d.left-f.left}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||s.body;a&&!/^body|html$/i.test(a.nodeName)&&c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(f){var e=this[0],j;if(!e)return null;if(f!==w)return this.each(function(){if(j=wa(this))j.scrollTo(!a?f:c(j).scrollLeft(),a?f:c(j).scrollTop());else this[d]=f});else return(j=wa(e))?"pageXOffset"in j?j[a?"pageYOffset": +"pageXOffset"]:c.support.boxModel&&j.document.documentElement[d]||j.document.body[d]:e[d]}});c.each(["Height","Width"],function(a,b){var d=b.toLowerCase();c.fn["inner"+b]=function(){return this[0]?c.css(this[0],d,false,"padding"):null};c.fn["outer"+b]=function(f){return this[0]?c.css(this[0],d,false,f?"margin":"border"):null};c.fn[d]=function(f){var e=this[0];if(!e)return f==null?null:this;if(c.isFunction(f))return this.each(function(j){var i=c(this);i[d](f.call(this,j,i[d]()))});return"scrollTo"in +e&&e.document?e.document.compatMode==="CSS1Compat"&&e.document.documentElement["client"+b]||e.document.body["client"+b]:e.nodeType===9?Math.max(e.documentElement["client"+b],e.body["scroll"+b],e.documentElement["scroll"+b],e.body["offset"+b],e.documentElement["offset"+b]):f===w?c.css(e,d):this.css(d,typeof f==="string"?f:f+"px")}});A.jQuery=A.$=c})(window); \ No newline at end of file diff --git a/includes/kohana/modules/userguide/media/js/kodoc.js b/includes/kohana/modules/userguide/media/js/kodoc.js new file mode 100644 index 00000000..b914d937 --- /dev/null +++ b/includes/kohana/modules/userguide/media/js/kodoc.js @@ -0,0 +1,94 @@ +$(document).ready(function() +{ + // Translation selector + $('#topbar form select').change(function() + { + $(this).parents('form').submit(); + }); + + // Syntax highlighter + $('pre:not(.debug) code').each(function() + { + $(this).addClass('brush: php'); + }); + + SyntaxHighlighter.config.tagName = 'code'; + // Don't show the toolbar or line-numbers. + SyntaxHighlighter.defaults.toolbar = false; + SyntaxHighlighter.defaults.gutter = false; + SyntaxHighlighter.all(); + + // Striped tables + $('#content tbody tr:even').addClass('alt'); + + // Toggle menus + $('#menu ol li strong').each(function() + { + var link = $(this); + var menu = link.parent().find('ol:first, ul:first'); + var togg = $('+').appendTo(link); + + link.click(function() + { + if (menu.is(':visible')) + { + // Hide visible menus + togg.html('+'); + menu.stop(true, true).slideUp('fast'); + } + else + { + // Show hidden menus + togg.html('–'); + menu.stop(true, true).slideDown('fast'); + } + }); + + // Hide all menus that do not contain the active link + menu.not(':has(a[href="'+ window.location.pathname +'"])').hide(); + + if (menu.is(':visible')) + { + // Display the toggle as being open + togg.html('–'); + } + }); + + // Collapsable class contents + $('#content #toc').each(function() + { + var header = $(this); + var content = $('#content div.toc').hide(); + + $('[ + ]').toggle(function() + { + $(this).html('[ – ]'); + content.stop(true, true).slideDown(); + }, + function() + { + $(this).html('[ + ]'); + content.stop(true, true).slideUp(); + }) + .appendTo(header); + }); + + // Show source links + $('#content .method-source').each(function() + { + var self = $(this); + var togg = $('+').appendTo($('h5', self)); + var code = self.find('pre').hide(); + + self.toggle(function() + { + togg.html('–'); + code.stop(true, true).slideDown(); + }, + function() + { + togg.html('+'); + code.stop(true, true).slideUp(); + }); + }); +}); diff --git a/includes/kohana/modules/userguide/media/js/shBrushPhp.js b/includes/kohana/modules/userguide/media/js/shBrushPhp.js new file mode 100644 index 00000000..4e92a19b --- /dev/null +++ b/includes/kohana/modules/userguide/media/js/shBrushPhp.js @@ -0,0 +1,91 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/ + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/wiki/SyntaxHighlighter:Donate + * + * @version + * 2.1.364 (October 15 2009) + * + * @copyright + * Copyright (C) 2004-2009 Alex Gorbatchev. + * + * @license + * This file is part of SyntaxHighlighter. + * + * SyntaxHighlighter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * SyntaxHighlighter is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with SyntaxHighlighter. If not, see . + */ +SyntaxHighlighter.brushes.Php = function() +{ + var funcs = 'abs acos acosh addcslashes addslashes ' + + 'array_change_key_case array_chunk array_combine array_count_values array_diff '+ + 'array_diff_assoc array_diff_key array_diff_uassoc array_diff_ukey array_fill '+ + 'array_filter array_flip array_intersect array_intersect_assoc array_intersect_key '+ + 'array_intersect_uassoc array_intersect_ukey array_key_exists array_keys array_map '+ + 'array_merge array_merge_recursive array_multisort array_pad array_pop array_product '+ + 'array_push array_rand array_reduce array_reverse array_search array_shift '+ + 'array_slice array_splice array_sum array_udiff array_udiff_assoc '+ + 'array_udiff_uassoc array_uintersect array_uintersect_assoc '+ + 'array_uintersect_uassoc array_unique array_unshift array_values array_walk '+ + 'array_walk_recursive atan atan2 atanh base64_decode base64_encode base_convert '+ + 'basename bcadd bccomp bcdiv bcmod bcmul bindec bindtextdomain bzclose bzcompress '+ + 'bzdecompress bzerrno bzerror bzerrstr bzflush bzopen bzread bzwrite ceil chdir '+ + 'checkdate checkdnsrr chgrp chmod chop chown chr chroot chunk_split class_exists '+ + 'closedir closelog copy cos cosh count count_chars date decbin dechex decoct '+ + 'deg2rad delete ebcdic2ascii echo empty end ereg ereg_replace eregi eregi_replace error_log '+ + 'error_reporting escapeshellarg escapeshellcmd eval exec exit exp explode extension_loaded '+ + 'feof fflush fgetc fgetcsv fgets fgetss file_exists file_get_contents file_put_contents '+ + 'fileatime filectime filegroup fileinode filemtime fileowner fileperms filesize filetype '+ + 'floatval flock floor flush fmod fnmatch fopen fpassthru fprintf fputcsv fputs fread fscanf '+ + 'fseek fsockopen fstat ftell ftok getallheaders getcwd getdate getenv gethostbyaddr gethostbyname '+ + 'gethostbynamel getimagesize getlastmod getmxrr getmygid getmyinode getmypid getmyuid getopt '+ + 'getprotobyname getprotobynumber getrandmax getrusage getservbyname getservbyport gettext '+ + 'gettimeofday gettype glob gmdate gmmktime ini_alter ini_get ini_get_all ini_restore ini_set '+ + 'interface_exists intval ip2long is_a is_array is_bool is_callable is_dir is_double '+ + 'is_executable is_file is_finite is_float is_infinite is_int is_integer is_link is_long '+ + 'is_nan is_null is_numeric is_object is_readable is_real is_resource is_scalar is_soap_fault '+ + 'is_string is_subclass_of is_uploaded_file is_writable is_writeable mkdir mktime nl2br '+ + 'parse_ini_file parse_str parse_url passthru pathinfo readlink realpath rewind rewinddir rmdir '+ + 'round str_ireplace str_pad str_repeat str_replace str_rot13 str_shuffle str_split '+ + 'str_word_count strcasecmp strchr strcmp strcoll strcspn strftime strip_tags stripcslashes '+ + 'stripos stripslashes stristr strlen strnatcasecmp strnatcmp strncasecmp strncmp strpbrk '+ + 'strpos strptime strrchr strrev strripos strrpos strspn strstr strtok strtolower strtotime '+ + 'strtoupper strtr strval substr substr_compare'; + + var keywords = 'and or xor array as break case ' + + 'cfunction class const continue declare default die do else ' + + 'elseif enddeclare endfor endforeach endif endswitch endwhile ' + + 'extends for foreach function include include_once global if ' + + 'new old_function return static switch use require require_once ' + + 'var while abstract interface public implements extends private protected throw'; + + var constants = '__FILE__ __LINE__ __METHOD__ __FUNCTION__ __CLASS__'; + + this.regexList = [ + { regex: SyntaxHighlighter.regexLib.singleLineCComments, css: 'comments' }, // one line comments + { regex: SyntaxHighlighter.regexLib.multiLineCComments, css: 'comments' }, // multiline comments + { regex: SyntaxHighlighter.regexLib.doubleQuotedString, css: 'string' }, // double quoted strings + { regex: SyntaxHighlighter.regexLib.singleQuotedString, css: 'string' }, // single quoted strings + { regex: /\$\w+/g, css: 'variable' }, // variables + { regex: new RegExp(this.getKeywords(funcs), 'gmi'), css: 'functions' }, // common functions + { regex: new RegExp(this.getKeywords(constants), 'gmi'), css: 'constants' }, // constants + { regex: new RegExp(this.getKeywords(keywords), 'gm'), css: 'keyword' } // keyword + ]; + + this.forHtmlScript(SyntaxHighlighter.regexLib.phpScriptTags); +}; + +SyntaxHighlighter.brushes.Php.prototype = new SyntaxHighlighter.Highlighter(); +SyntaxHighlighter.brushes.Php.aliases = ['php']; diff --git a/includes/kohana/modules/userguide/media/js/shCore.js b/includes/kohana/modules/userguide/media/js/shCore.js new file mode 100644 index 00000000..5fed4860 --- /dev/null +++ b/includes/kohana/modules/userguide/media/js/shCore.js @@ -0,0 +1,30 @@ +/** + * SyntaxHighlighter + * http://alexgorbatchev.com/ + * + * SyntaxHighlighter is donationware. If you are using it, please donate. + * http://alexgorbatchev.com/wiki/SyntaxHighlighter:Donate + * + * @version + * 2.1.364 (October 15 2009) + * + * @copyright + * Copyright (C) 2004-2009 Alex Gorbatchev. + * + * @license + * This file is part of SyntaxHighlighter. + * + * SyntaxHighlighter is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * SyntaxHighlighter is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with SyntaxHighlighter. If not, see . + */ +eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('c(!1q.2X){h 2X=l(){h p={6b:{"1s-R":"","84-2y":1,"83-2y-7W":I,"1I":v,"8d-89":I,"1H-2Q":4,"3g":I,"1w":I,"66":N,"8k-8l":I,"88":N,"5h-1p":I,"1L-1l":N},M:{4T:I,69:v,5l:16,5k:16,8B:N,8f:N,8s:"54",1f:{5q:"53 1g",5d:"9N 1g",5i:"9O 6q 6p",78:"9M C 9L 1V 9I 6p 9J",3t:"3t",6C:"?",1A:"2X\\n\\n",6T:"9K\'t 9P 2O D: ",7x:"9Q 9W\'t 9X D 1L-1l 9V: ",77:"<1L 9t=\\"2s://5x.6x.6B/9s/9x\\"><6z><9y 2s-9E=\\"9F-9D\\" 63=\\"1X/1L; 9C=9z-8\\" /><3i>9A 2X<33 1m=\\"39-9Y:9Z,an,ao,am-al;ai-3f:#aj;3f:#ak;39-2Q:ap;1X-6G:6u;\\">2X6O 2.1.ag (a5 15 6h)2s://6I.3ka3 a0 a1 f 1l, a2 8R 6q 8Q 8O 8W!8V C 8U 8X.8K 8N-6h 8M 8S."},8u:N},1r:{4Z:v,9o:v,3m:v,6f:{}},2d:{},8h:{9g:/\\/\\*[\\s\\S]*?\\*\\//3b,9d:/\\/\\/.*$/3b,9e:/#.*$/3b,9j:/"([^\\\\"\\n]|\\\\.)*"/g,9n:/\'([^\\\\\'\\n]|\\\\.)*\'/g,9m:/"([^\\\\"]|\\\\.)*"/g,8Y:/\'([^\\\\\']|\\\\.)*\'/g,9k:/(&X;|<)!--[\\s\\S]*?--(&Z;|>)/3b,43:/&X;\\w+:\\/\\/[\\w-.\\/?%&=@:;]*&Z;|\\w+:\\/\\/[\\w-.\\/?%&=@:;]*/g,9c:{G:/(&X;|<)\\?=?/g,1d:/\\?(&Z;|>)/g},93:{G:/(&X;|<)%=?/g,1d:/%(&Z;|>)/g},92:{G:/(&X;|<)\\s*1l.*?(&Z;|>)/4e,1d:/(&X;|<)\\/\\s*1l\\s*(&Z;|>)/4e}},1w:{1c:l(3O){h 3T=Q.22("3Y"),5s=p.1w.7d;3T.L="1w";D(h 30 1V 5s){h 6i=5s[30],5t=W 6i(3O),1Y=5t.1c();3O.6g[30]=5t;c(1Y==v){1J}c(7X(1Y)=="91"){1Y=p.1w.6m(1Y,3O.1k,30)}1Y.L+="5v "+30;3T.2p(1Y)}q 3T},6m:l(5L,7j,5K){h a=Q.22("a"),5N=a.1m,5D=p.M,5M=5D.5l,5J=5D.5k;a.27="#"+5K;a.3i=5L;a.5j=7j;a.76=5K;a.1Q=5L;c(40(5M)==N){5N.26=5M+"75"}c(40(5J)==N){5N.2e=5J+"75"}a.9l=l(e){97{p.1w.6M(f,e||1q.6Y,f.5j,f.76)}98(e){p.B.1A(e.6n)}q N};q a},6M:l(7i,7g,7b,7h,7f){h 5G=p.1r.6f[7b],5H;c(5G==v||(5H=5G.6g[7h])==v){q v}q 5H.2z(7i,7g,7f)},7d:{5q:l(5b){f.1c=l(){c(5b.V("66")!=I){q}q p.M.1f.5q};f.2z=l(5c,8T,8P){h A=5b.A;5c.7y.4p(5c);A.L=A.L.E("5O","")}},5d:l(6R){f.1c=l(){q p.M.1f.5d};f.2z=l(b1,bU,bV){h 3J=p.B.3d(6R.5g).E(/"+3J+"");2A.Q.4o()}},5i:l(64){h 3C,c2,6a=64.1k;f.1c=l(){h 2V=p.M;c(2V.69==v){q v}l 1E(56){h 5m="";D(h 5f 1V 56){5m+=""}q 5m};l 2i(5n){h 5p="";D(h 5o 1V 5n){5p+=" "+5o+"=\'"+5n[5o]+"\'"}q 5p};h 67={26:2V.5l,2e:2V.5k,1k:6a+"bY",4r:"bZ/x-71-6V",3i:p.M.1f.5i},5V={bE:"ay",bD:"bC",bA:"5j="+6a,c4:"N"},5U=2V.69,3x;c(/bG/i.1R(6K.7k)){3x="<4h"+2i({bH:"bM:bN-bL-bK-bI-bJ",c3:"2s://ck.cj.3k/cm/71/c9/6V/c8.c7#6O=9,0,0,0"})+2i(67)+">"+1E(5V)+1E({c6:5U})+""}F{3x=""}3C=Q.22("A");3C.1Q=3x;q 3C};f.2z=l(cf,ce,62){h 7c=62.cd;6U(7c){2K"7q":h 61=p.B.2T(p.B.3d(64.5g).E(/&X;/g,"<").E(/&Z;/g,">").E(/&aT;/g,"&"));c(1q.74){1q.74.aU("1X",61)}F{q p.B.2T(61)}2K"aR":p.B.1A(p.M.1f.78);2h;2K"aP":p.B.1A(62.6n);2h}}},aV:l(65){f.1c=l(){q p.M.1f.3t};f.2z=l(aW,bz,b0){h 1Z=Q.22("aZ"),1N=v;c(p.1r.3m!=v){Q.33.4p(p.1r.3m)}p.1r.3m=1Z;1Z.1m.aX="aY:aO;26:6r;2e:6r;G:-6j;4w:-6j;";Q.33.2p(1Z);1N=1Z.5Q.Q;6J(1N,1q.Q);1N.3D(""+65.A.1Q+"");1N.4o();1Z.5Q.4F();1Z.5Q.3t();l 6J(6N,6E){h 2I=6E.4O("4n");D(h i=0;i<2I.u;i++){c(2I[i].6y.6P()=="6A"&&/aE\\.1a$/.1R(2I[i].27)){6N.3D("<4n 4r=\\"1X/1a\\" 6y=\\"6A\\" 27=\\""+2I[i].27+"\\">")}}}}},az:l(aA){f.1c=l(){q p.M.1f.6C};f.2z=l(aF,aG){h 2A=p.B.4z("","4k",aM,aK,"7a=0"),1N=2A.Q;1N.3D(p.M.1f.77);1N.4o();2A.4F()}}}},B:{Y:l(49,73,3y){3y=3e.aH(3y||0,0);D(h i=3y;i<49.u;i++){c(49[i]==73){q i}}q-1},6d:l(72){q 72+3e.aI(3e.b2()*b3).2u()},6c:l(51,4L){h 3h={},1W;D(1W 1V 51){3h[1W]=51[1W]}D(1W 1V 4L){3h[1W]=4L[1W]}q 3h},80:l(4J){6U(4J){2K"I":q I;2K"N":q N}q 4J},4z:l(43,6W,4B,4H,2N){h x=(6X.26-4B)/2,y=(6X.2e-4H)/2;2N+=", G="+x+", 4w="+y+", 26="+4B+", 2e="+4H;2N=2N.E(/^,/,"");h 4E=1q.bk(43,6W,2N);4E.4F();q 4E},7C:l(1G,1T,1U){c(1G.6Z){1G["e"+1T+1U]=1U;1G[1T+1U]=l(){1G["e"+1T+1U](1q.6Y)};1G.6Z("bw"+1T,1G[1T+1U])}F{1G.bv(1T,1U,N)}},1A:l(z){1A(p.M.1f.1A+z)},4u:l(4N,6Q){h 2r=p.1r.4Z,3V=v;c(2r==v){2r={};D(h 2L 1V p.2d){h 42=p.2d[2L].bu;c(42==v){1J}p.2d[2L].R=2L.6P();D(h i=0;i<42.u;i++){2r[42[i]]=2L}}p.1r.4Z=2r}3V=p.2d[2r[4N]];c(3V==v&&6Q!=N){p.B.1A(p.M.1f.6T+4N)}q 3V},46:l(z,6S){h 2E=z.1P("\\n");D(h i=0;i<2E.u;i++){2E[i]=6S(2E[i])}q 2E.5A("\\n")},8C:l(z){q z.E(/^[ ]*[\\n]+|[\\n]*[ ]*$/g,"")},8H:l(z){h 3X,45={},4P=W U("^\\\\[(?<4c>(.*?))\\\\]$"),7e=W U("(?[\\\\w-]+)"+"\\\\s*:\\\\s*"+"(?<24>"+"[\\\\w-%#]+|"+"\\\\[.*?\\\\]|"+"\\".*?\\"|"+"\'.*?\'"+")\\\\s*;?","g");2j((3X=7e.T(z))!=v){h 2f=3X.24.E(/^[\'"]|[\'"]$/g,"");c(2f!=v&&4P.1R(2f)){h m=4P.T(2f);2f=m.4c.u>0?m.4c.1P(/\\s*,\\s*/):[]}45[3X.R]=2f}q 45},7K:l(z,1a){c(z==v||z.u==0||z=="\\n"){q z}z=z.E(/"+2l+""})}q z},7V:l(6l,6o){h 32=6l.2u();2j(32.u<6o){32="0"+32}q 32},6k:l(){h 3w=Q.22("A"),3B,3o=0,44=Q.33,1k=p.B.6d("6k"),36="",4U="";3w.1Q=36+"6e\\">"+36+"1p\\">"+36+"2y\\">"+36+"63"+"\\"><4G 1s=\\"b5\\"><4G 1k=\\""+1k+"\\">&2B;"+4U+4U+2Y+2Y+2Y+2Y;44.2p(3w);3B=Q.bb(1k);c(/bg/i.1R(6K.7k)){h 6v=1q.be(3B,v);3o=85(6v.bc("26"))}F{3o=3B.bd}44.4p(3w);q 3o},8b:l(79,6s){h 1H="";D(h i=0;i<6s;i++){1H+=" "}q 79.E(/\\t/g,1H)},8a:l(2Z,4f){h bF=2Z.1P("\\n"),1H="\\t",4d="";D(h i=0;i<50;i++){4d+=" "}l 8x(3s,18,8A){q 3s.29(0,18)+4d.29(0,8A)+3s.29(18+1,3s.u)};2Z=p.B.46(2Z,l(20){c(20.Y(1H)==-1){q 20}h 18=0;2j((18=20.Y(1H))!=-1){h 8w=4f-18%4f;20=8x(20,18,8w)}q 20});q 2Z},3d:l(z){h br=/|&X;br\\s*\\/?&Z;/4e;c(p.M.8B==I){z=z.E(br,"\\n")}c(p.M.8f==I){z=z.E(br,"")}q z},2G:l(z){q z.E(/^\\s+|\\s+$/g,"")},2T:l(z){h 21=p.B.3d(z).1P("\\n"),bf=W bh(),8D=/^\\s*/,2a=ba;D(h i=0;i<21.u&&2a>0;i++){h 4x=21[i];c(p.B.2G(4x).u==0){1J}h 4I=8D.T(4x);c(4I==v){q z}2a=3e.2a(4I[0].u,2a)}c(2a>0){D(h i=0;i<21.u;i++){21[i]=21[i].29(2a)}}q 21.5A("\\n")},82:l(35,31){c(35.H<31.H){q-1}F{c(35.H>31.H){q 1}F{c(35.u<31.u){q-1}F{c(35.u>31.u){q 1}}}}q 0},2D:l(8q,34){l 8n(4D,8r){q[W p.4v(4D[0],4D.H,8r.1a)]};h b4=0,4s=v,3L=[],8p=34.4X?34.4X:8n;2j((4s=34.3K.T(8q))!=v){3L=3L.2t(8p(4s,34))}q 3L},8m:l(8o){h X="&X;",Z="&Z;";q 8o.E(p.8h.43,l(m){h 4j="",47="";c(m.Y(X)==0){47=X;m=m.3U(X.u)}c(m.Y(Z)==m.u-Z.u){m=m.3U(0,m.u-Z.u);4j=Z}q 47+""+m+""+4j})},8v:l(){h 3N=Q.4O("1l"),4i=[];D(h i=0;i<3N.u;i++){c(3N[i].4r=="6e"){4i.K(3N[i])}}q 4i},8I:l(4b){h 4q="",1v=p.B.2G(4b),3R=N;c(1v.Y(4q)==0){1v=1v.3U(4q.u);3R=I}c(1v.Y(3S)==1v.u-3S.u){1v=1v.3U(0,1v.u-3S.u);3R=I}q 3R?1v:4b}},1I:l(8E,4R){l 8e(4g){h 4Q=[];D(h i=0;i<4g.u;i++){4Q.K(4g[i])}q 4Q};h 2q=4R?[4R]:8e(Q.4O(p.M.8s)),8J="1Q",2k=v,4S=p.M;c(4S.4T){2q=2q.2t(p.B.8v())}c(2q.u===0){q}D(h i=0;i<2q.u;i++){h 2M=2q[i],28=p.B.8H(2M.L),1D,2W,25;28=p.B.6c(8E,28);1D=28["2O"];c(1D==v){1J}c(28["1L-1l"]=="I"||p.6b["1L-1l"]==I){2k=W p.4a(1D);1D="b9"}F{h 3P=p.B.4u(1D);c(3P){1D=3P.R;2k=W 3P()}F{1J}}2W=2M[8J];c(4S.4T){2W=p.B.8I(2W)}28["2O-R"]=1D;2k.1I(2W,28);25=2k.A;c(p.M.8u){25=Q.22("bj");25.24=2k.A.1Q;25.1m.26="bt";25.1m.2e="bx"}2M.7y.bs(25,2M)}},bq:l(7H){p.B.7C(1q,"bl",l(){p.1I(7H)})}};p.4v=l(4A,7G,1a){f.24=4A;f.H=7G;f.u=4A.u;f.1a=1a;f.5Y=v};p.4v.14.2u=l(){q f.24};p.4a=l(4K){h 3z=p.B.4u(4K),2g,4W=W p.2d.bm(),bn=v;c(3z==v){q}2g=W 3z();f.4m=4W;c(2g.3I==v){p.B.1A(p.M.1f.7x+4K);q}4W.59.K({3K:2g.3I.C,4X:7p});l 3A(4Y,7w){D(h j=0;j<4Y.u;j++){4Y[j].H+=7w}};l 7p(19,bp){h 7n=19.C,1o=[],4M=2g.59,7l=19.H+19.G.u,2U=2g.3I,1n;D(h i=0;i<4M.u;i++){1n=p.B.2D(7n,4M[i]);3A(1n,7l);1o=1o.2t(1n)}c(2U.G!=v&&19.G!=v){1n=p.B.2D(19.G,2U.G);3A(1n,19.H);1o=1o.2t(1n)}c(2U.1d!=v&&19.1d!=v){1n=p.B.2D(19.1d,2U.1d);3A(1n,19.H+19[0].bo(19.1d));1o=1o.2t(1n)}D(h j=0;j<1o.u;j++){1o[j].5Y=3z.R}q 1o}};p.4a.14.1I=l(7t,7s){f.4m.1I(7t,7s);f.A=f.4m.A};p.7I=l(){};p.7I.14={V:l(7J,7Z){h 4l=f.1E[7J];q p.B.80(4l==v?7Z:4l)},1c:l(7Y){q Q.22(7Y)},8i:l(2F,81){h 3u=[];c(2F!=v){D(h i=0;i<2F.u;i++){c(7X(2F[i])=="4h"){3u=3u.2t(p.B.2D(81,2F[i]))}}}q 3u.aB(p.B.82)},86:l(){h 23=f.2C;D(h i=0;i<23.u;i++){c(23[i]===v){1J}h 2x=23[i],4V=2x.H+2x.u;D(h j=i+1;j<23.u&&23[i]!==v;j++){h 1S=23[j];c(1S===v){1J}F{c(1S.H>4V){2h}F{c(1S.H==2x.H&&1S.u>2x.u){f.2C[i]=v}F{c(1S.H>=2x.H&&1S.H<4V){f.2C[j]=v}}}}}}},8t:l(2H){h 3r=2H.1P(/\\n/g),3n=85(f.V("84-2y")),2v=f.V("83-2y-7W"),7N=f.V("1I",[]),7U=f.V("3g");2H="";c(2v==I){2v=(3n+3r.u-1).2u().u}F{c(40(2v)==I){2v=0}}D(h i=0;i<3r.u;i++){h 1x=3r[i],60=/^(&2B;|\\s)+/.T(1x),52="aN"+(i%2==0?1:2),7F=p.B.7V(3n+i,2v),7P=p.B.Y(7N,(3n+i).2u())!=-1,2S=v;c(60!=v){2S=60[0].2u();1x=1x.29(2S.u)}1x=p.B.2G(1x);c(1x.u==0){1x="&2B;"}c(7P){52+=" aQ"}2H+=""+"<7L>"+"<7T>"+(7U?"<3F 1s=\\"aS\\">"+7F+"":"")+"<3F 1s=\\"63\\">"+(2S!=v?""+2S.E(" ","&2B;")+"":"")+1x+""+""+""+""}q 2H},8y:l(5X,5T){h 18=0,3c="",3a=p.B.7K,5S=f.V("2O-R","");l 5W(5Z){h 5R=5Z?(5Z.5Y||5S):5S;q 5R?5R+" ":""};D(h i=0;i<5T.u;i++){h 1y=5T[i],3G;c(1y===v||1y.u===0){1J}3G=5W(1y);3c+=3a(5X.29(18,1y.H-18),3G+"7O")+3a(1y.24,3G+1y.1a);18=1y.H+1y.u}3c+=3a(5X.29(18),5W()+"7O");q 3c},1I:l(C,7E){h cb=p.M,1r=p.1r,A,ci,3Z,ch="cn";f.1E={};f.A=v;f.1p=v;f.C=v;f.1i=v;f.6g={};f.1k=p.B.6d("cl");1r.6f[f.1k]=f;c(C===v){C=""}f.1E=p.B.6c(p.6b,7E||{});c(f.V("88")==I){f.1E.1w=f.1E.3g=N}f.A=A=f.1c("3Y");f.1p=f.1c("3Y");f.1p.L="1p";L="6e";A.1k=f.1k;c(f.V("66")){L+=" 5O"}c(f.V("3g")==N){L+=" bB"}c(f.V("5h-1p")==N){f.1p.L+=" bO-5h"}L+=" "+f.V("1s-R");L+=" "+f.V("2O-R");A.L=L;f.5g=C;f.C=p.B.8C(C).E(/\\r/g," ");3Z=f.V("1H-2Q");f.C=f.V("8d-89")==I?p.B.8a(f.C,3Z):p.B.8b(f.C,3Z);f.C=p.B.2T(f.C);c(f.V("1w")){f.1i=f.1c("3Y");f.1i.L="1i";f.1i.2p(p.1w.1c(f));A.2p(f.1i);h 1i=f.1i;l 58(){1i.L=1i.L.E("53","")};A.c0=l(){58();1i.L+=" 53"};A.bX=l(){58()}}A.2p(f.1p);f.2C=f.8i(f.59,f.C);f.86();C=f.8y(f.C,f.2C);C=f.8t(p.B.2G(C));c(f.V("8k-8l")){C=p.B.8m(C)}f.1p.1Q=C},9f:l(z){z=z.E(/^\\s+|\\s+$/g,"").E(/\\s+/g,"|");q"\\\\b(?:"+z+")\\\\b"},9i:l(2J){f.3I={G:{3K:2J.G,1a:"1l"},1d:{3K:2J.1d,1a:"1l"},C:W U("(?"+2J.G.1g+")"+"(?.*?)"+"(?<1d>"+2J.1d.1g+")","96")}}};q p}()}c(!1q.U){(l(){h 2w={T:10.14.T,87:5I.14.87,E:5I.14.E,1P:5I.14.1P},1F={13:/(?:[^\\\\([#\\s.]+|\\\\(?!k<[\\w$]+>|[7z]{[^}]+})[\\S\\s]?|\\((?=\\?(?!#|<[\\w$]+>)))+|(\\()(?:\\?(?:(#)[^)]*\\)|<([$\\w]+)>))?|\\\\(?:k<([\\w$]+)>|[7z]{([^}]+)})|(\\[\\^?)|([\\S\\s])/g,99:/(?:[^$]+|\\$(?![1-9$&`\']|{[$\\w]+}))+|\\$(?:([1-9]\\d*|[$&`\'])|{([$\\w]+)})/g,37:/^(?:\\s+|#.*)+/,5B:/^(?:[?*+]|{\\d+(?:,\\d*)?})/,7Q:/&&\\[\\^?/g,7S:/]/g},7o=l(5C,5v,5u){D(h i=5u||0;i<5C.u;i++){c(5C[i]===5v){q i}}q-1},8G=/()??/.T("")[1]!==3j,3q={};U=l(1e,1O){c(1e 68 10){c(1O!==3j){3H 7r("4y\'t 4C 9a 8z 95 7u 10 5u 94")}q 1e.3E()}h 1O=1O||"",7R=1O.Y("s")>-1,7M=1O.Y("x")>-1,5z=N,3v=[],1b=[],13=1F.13,J,cc,38,3M,3p;13.O=0;2j(J=2w.T.2n(13,1e)){c(J[2]){c(!1F.5B.1R(1e.17(13.O))){1b.K("(?:)")}}F{c(J[1]){3v.K(J[3]||v);c(J[3]){5z=I}1b.K("(")}F{c(J[4]){3M=7o(3v,J[4]);1b.K(3M>-1?"\\\\"+(3M+1)+(40(1e.5w(13.O))?"":"(?:)"):J[0])}F{c(J[5]){1b.K(3q.7m?3q.7m.7q(J[5],J[0].5w(1)==="P"):J[0])}F{c(J[6]){c(1e.5w(13.O)==="]"){1b.K(J[6]==="["?"(?!)":"[\\\\S\\\\s]");13.O++}F{cc=U.8g("&&"+1e.17(J.H),1F.7Q,1F.7S,"",{7D:"\\\\"})[0];1b.K(J[6]+cc+"]");13.O+=cc.u+1}}F{c(J[7]){c(7R&&J[7]==="."){1b.K("[\\\\S\\\\s]")}F{c(7M&&1F.37.1R(J[7])){38=2w.T.2n(1F.37,1e.17(13.O-1))[0].u;c(!1F.5B.1R(1e.17(13.O-1+38))){1b.K("(?:)")}13.O+=38-1}F{1b.K(J[7])}}}F{1b.K(J[0])}}}}}}}3p=10(1b.5A(""),2w.E.2n(1O,/[9B]+/g,""));3p.1C={1g:1e,2m:5z?3v:v};q 3p};U.9q=l(R,o){3q[R]=o};10.14.T=l(z){h 1h=2w.T.2n(f,z),R,i,5y;c(1h){c(8G&&1h.u>1){5y=W 10("^"+f.1g+"$(?!\\\\s)",f.5E());2w.E.2n(1h[0],5y,l(){D(i=1;i<8j.u-2;i++){c(8j[i]===3j){1h[i]=3j}}})}c(f.1C&&f.1C.2m){D(i=1;i<1h.u;i++){R=f.1C.2m[i-1];c(R){1h[R]=1h[i]}}}c(f.3l&&f.O>(1h.H+1h[0].u)){f.O--}}q 1h}})()}10.14.5E=l(){q(f.3l?"g":"")+(f.av?"i":"")+(f.8F?"m":"")+(f.37?"x":"")+(f.a4?"y":"")};10.14.3E=l(7A){h 5F=W U(f.1g,(7A||"")+f.5E());c(f.1C){5F.1C={1g:f.1C.1g,2m:f.1C.2m?f.1C.2m.17(0):v}}q 5F};10.14.2n=l(90,z){q f.T(z)};10.14.9b=l(9h,8c){q f.T(8c[0])};U.5P=l(57,5e){h 55="/"+57+"/"+(5e||"");q U.5P[55]||(U.5P[55]=W U(57,5e))};U.41=l(z){q z.E(/[-[\\]{}()*+?.\\\\^$|,#\\s]/g,"\\\\$&")};U.8g=l(z,G,11,1j,2R){h 2R=2R||{},2P=2R.7D,12=2R.c5,1j=1j||"",5r=1j.Y("g")>-1,70=1j.Y("i")>-1,7v=1j.Y("m")>-1,5a=1j.Y("y")>-1,1j=1j.E(/y/g,""),G=G 68 10?(G.3l?G:G.3E("g")):W U(G,"g"+1j),11=11 68 10?(11.3l?11:11.3E("g")):W U(11,"g"+1j),1M=[],2o=0,1u=0,1t=0,1z=0,2b,2c,1B,1K,3Q,48;c(2P){c(2P.u>1){3H aC("4y\'t 4C aL aJ 7u 41 7B")}c(7v){3H 7r("4y\'t 4C 41 7B 8z bi b8 8F b7")}3Q=U.41(2P);48=W 10("^(?:"+3Q+"[\\\\S\\\\s]|(?:(?!"+G.1g+"|"+11.1g+")[^"+3Q+"])+)+",70?"i":"")}2j(I){G.O=11.O=1t+(2P?(48.T(z.17(1t))||[""])[0].u:0);1B=G.T(z);1K=11.T(z);c(1B&&1K){c(1B.H<=1K.H){1K=v}F{1B=v}}c(1B||1K){1u=(1B||1K).H;1t=(1B?G:11).O}F{c(!2o){2h}}c(5a&&!2o&&1u>1z){2h}c(1B){c(!2o++){2b=1u;2c=1t}}F{c(1K&&2o){c(!--2o){c(12){c(12[0]&&2b>1z){1M.K([12[0],z.17(1z,2b),1z,2b])}c(12[1]){1M.K([12[1],z.17(2b,2c),2b,2c])}c(12[2]){1M.K([12[2],z.17(2c,1u),2c,1u])}c(12[3]){1M.K([12[3],z.17(1u,1t),1u,1t])}}F{1M.K(z.17(2c,1u))}1z=1t;c(!5r){2h}}}F{G.O=11.O=0;3H bP("8L aq 9r ar 8Z")}}c(1u===1t){1t++}}c(5r&&!5a&&12&&12[0]&&z.u>1z){1M.K([12[0],z.17(1z),1z,z.u])}G.O=11.O=0;q 1M};',62,768,'||||||||||||if|||this||var||||function||||sh|return||||length|null||||str|div|utils|code|for|replace|else|left|index|true|_121|push|className|config|false|lastIndex||document|name||exec|XRegExp|getParam|new|lt|indexOf|gt|RegExp|_139|vN|part|prototype|||slice|pos|_d3|css|_11f|create|right|_119|strings|source|_129|bar|_13a|id|script|style|_da|_d6|lines|window|vars|class|_145|_144|_b5|toolbar|_f4|_103|_146|alert|_149|_x|_c3|params|lib|obj|tab|highlight|continue|_14a|html|_142|doc|_11a|split|innerHTML|test|_ec|_5a|_5b|in|_4f|text|_8|_3c|_91|_98|createElement|_e7|value|_c5|width|href|_c2|substr|min|_147|_148|brushes|height|_6e|_cd|break|attributes|while|_be|_75|captureNames|call|_143|appendChild|_bc|_5f|http|concat|toString|_f0|real|_e9|line|execute|wnd|nbsp|matches|getMatches|_66|_e3|trim|_ed|_40|_10f|case|_61|_c1|_55|brush|_13c|size|_13b|_f9|unindent|_d9|_28|_c4|SyntaxHighlighter|_81|_88|_5|m2|_7a|body|_a2|m1|_80|extended|len|font|_fe|gm|_fd|fixInputString|Math|color|gutter|_4e|title|undefined|com|global|printFrame|_ef|_7d|_125|_118|_ee|_8e|print|_e5|_11e|_7b|_32|_49|_cc|offsetMatches|_7c|_25|write|addFlags|td|_104|throw|htmlScript|_22|regex|_a7|_124|_af|_2|_c6|_14b|_b6|_b4|_3|substring|_60|_76|_6a|DIV|_10b|isNaN|escape|_62|url|_7e|_6b|eachLine|_ae|esc|_47|HtmlScript|_b2|values|_8c|gi|_89|_b9|object|_b0|_ad|_blank|_e1|xmlBrush|link|close|removeChild|_b3|type|_a6|_73|findBrush|Match|top|_9d|can|popup|_c8|_53|supply|_a3|win|focus|span|_54|_9e|_50|_cb|_4d|_d7|_5d|getElementsByTagName|_6c|_ba|_b8|_bf|useScriptTags|_82|_ea|_ce|func|_d0|discoveredBrushes||_4c|_f6|show|pre|key|_29|_133|hide|regexList|_141|_19|_1a|viewSource|_134|_2b|originalCode|wrap|copyToClipboard|highlighterId|toolbarItemHeight|toolbarItemWidth|_2a|_2c|_2e|_2d|expandSource|_13e|_4|_7|from|item|charAt|www|r2|_11d|join|quantifier|_113|_e|getNativeFlags|_12e|_17|_18|String|_10|_b|_9|_f|_d|collapsed|cache|contentWindow|_101|_ff|_fb|swf|_30|getBrushNameCss|_fa|brushName|_100|_f5|_37|_35|content|_24|_38|collapse|_2f|instanceof|clipboardSwf|_27|defaults|merge|guid|syntaxhighlighter|highlighters|toolbarCommands|2009|_6|500px|measureSpace|_78|createButton|message|_79|clipboard|to|0px|_85|decoration|center|_83|margin|w3|rel|head|stylesheet|org|help|xhtml1|_3f|0099FF|align|DTD|alexgorbatchev|copyStyles|navigator|none|executeCommand|_3e|version|toLowerCase|_5e|_1e|_65|noBrush|switch|flash|_52|screen|event|attachEvent|_13f|shockwave|_4b|_48|clipboardData|px|commandName|aboutDialog|copyToClipboardConfirmation|_84|scrollbars|_14|_36|items|_6d|_16|_13|_15|_12|_a|userAgent|_d8|unicode|_d5|_112|process|get|TypeError|_de|_dd|one|_140|_d1|brushNotHtmlScript|parentNode|pP|_12d|character|addEvent|escapeChar|_106|_f7|_c9|_c7|Highlighter|_df|decorate|table|_11c|_f1|plain|_f8|classLeft|_11b|classRight|tr|_f2|padNumber|numbers|typeof|_e2|_e0|toBoolean|_e4|matchesSortCallback|pad|first|parseInt|removeNestedMatches|match|light|tabs|processSmartTabs|processTabs|args|smart|toArray|stripBrs|matchRecursive|regexLib|findMatches|arguments|auto|links|processUrls|defaultAdd|_a9|_a8|_a1|_a4|tagName|createDisplayLines|debug|getSyntaxHighlighterScriptTags|_93|insertSpaces|processMatches|when|_90|bloggerMode|trimFirstAndLastLines|_9a|_b7|multiline|_117|parseParams|stripCData|_bd|Copyright|subject|Alex|2004|development|_1c|keep|donate|Gorbatchev|_1b|syntax|JavaScript|active|highlighter|multiLineSingleQuotedString|delimiters|_12f|string|scriptScriptTags|aspScriptTags|another|constructing|sgi|try|catch|replaceVar|flags|apply|phpScriptTags|singleLineCComments|singleLinePerlComments|getKeywords|multiLineCComments|_131|forHtmlScript|doubleQuotedString|xmlComments|onclick|multiLineDoubleQuotedString|singleQuotedString|spaceWidth|bottom|addPlugin|contains|1999|xmlns|dtd|TR|transitional|xhtml|meta|utf|About|sx|charset|Type|equiv|Content|EN|Transitional|your|now|Can|is|The|view|copy|find|Brush|PUBLIC|W3C|XHTML|DOCTYPE|option|wasn|configured|family|Geneva|you|like|please|If|sticky|October|target|https|paypal|_s|xclick|hosted_button_id|cmd|webscr|cgi|bin|364|4em|background|fff|000|serif|sans|Arial|Helvetica|1em|data|unbalanced|75em|large|xx|ignoreCase|3em|2930402|always|about|_42|sort|SyntaxError|printing|shCore|_43|_44|max|round|than|250|more|500|alt|absolute|error|highlighted|ok|number|amp|setData|printSource|_39|cssText|position|IFRAME|_3b|_1f|random|1000000|_a5|block|CDATA|flag|the|htmlscript|1000|getElementById|getPropertyValue|offsetWidth|getComputedStyle|_99|opera|Array|using|textarea|open|load|Xml|_cf|lastIndexOf|_d4|all||replaceChild|70em|aliases|addEventListener|on|30em|spaces|_3a|flashVars|nogutter|transparent|wmode|allowScriptAccess|_8a|msie|classid|96b8|444553540000|11cf|ae6d|clsid|d27cdb6e|no|Error|location|resizable|400|750|_20|_21|menubar|onmouseout|_clipboard|application|onmouseover|param|_26|codebase|menu|valueNames|movie|cab|swflash|cabs|embed|conf||command|_34|_33|src|_10c|_10a|macromedia|download|highlighter_|pub|important'.split('|'),0,{})) diff --git a/includes/kohana/modules/userguide/messages/userguide.php b/includes/kohana/modules/userguide/messages/userguide.php new file mode 100644 index 00000000..5397d2ea --- /dev/null +++ b/includes/kohana/modules/userguide/messages/userguide.php @@ -0,0 +1,14 @@ + array( + 'de-de' => 'Deutsch', + 'en-us' => 'English', + 'es-es' => 'Español', + 'zh-cn' => '简体中文', + 'ru-ru' => 'Русский', + 'fr-fr' => 'Français', + 'he-il' => 'עברית', + 'nl' => 'Nederlands', + ), +); diff --git a/includes/kohana/modules/userguide/vendor/markdown/License.text b/includes/kohana/modules/userguide/vendor/markdown/License.text new file mode 100755 index 00000000..52c868b5 --- /dev/null +++ b/includes/kohana/modules/userguide/vendor/markdown/License.text @@ -0,0 +1,36 @@ +PHP Markdown & Extra +Copyright (c) 2004-2008 Michel Fortin + +All rights reserved. + +Based on Markdown +Copyright (c) 2003-2006 John Gruber + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name "Markdown" nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +This software is provided by the copyright holders and contributors "as +is" and any express or implied warranties, including, but not limited +to, the implied warranties of merchantability and fitness for a +particular purpose are disclaimed. In no event shall the copyright owner +or contributors be liable for any direct, indirect, incidental, special, +exemplary, or consequential damages (including, but not limited to, +procurement of substitute goods or services; loss of use, data, or +profits; or business interruption) however caused and on any theory of +liability, whether in contract, strict liability, or tort (including +negligence or otherwise) arising in any way out of the use of this +software, even if advised of the possibility of such damage. diff --git a/includes/kohana/modules/userguide/vendor/markdown/markdown.php b/includes/kohana/modules/userguide/vendor/markdown/markdown.php new file mode 100755 index 00000000..b649f6c1 --- /dev/null +++ b/includes/kohana/modules/userguide/vendor/markdown/markdown.php @@ -0,0 +1,2909 @@ + +# +# Original Markdown +# Copyright (c) 2004-2006 John Gruber +# +# + + +define( 'MARKDOWN_VERSION', "1.0.1m" ); # Sat 21 Jun 2008 +define( 'MARKDOWNEXTRA_VERSION', "1.2.3" ); # Wed 31 Dec 2008 + + +# +# Global default settings: +# + +# Change to ">" for HTML output +@define( 'MARKDOWN_EMPTY_ELEMENT_SUFFIX', " />"); + +# Define the width of a tab for code blocks. +@define( 'MARKDOWN_TAB_WIDTH', 4 ); + +# Optional title attribute for footnote links and backlinks. +@define( 'MARKDOWN_FN_LINK_TITLE', "" ); +@define( 'MARKDOWN_FN_BACKLINK_TITLE', "" ); + +# Optional class attribute for footnote links and backlinks. +@define( 'MARKDOWN_FN_LINK_CLASS', "" ); +@define( 'MARKDOWN_FN_BACKLINK_CLASS', "" ); + + +# +# WordPress settings: +# + +# Change to false to remove Markdown from posts and/or comments. +@define( 'MARKDOWN_WP_POSTS', true ); +@define( 'MARKDOWN_WP_COMMENTS', true ); + + + +### Standard Function Interface ### + +@define( 'MARKDOWN_PARSER_CLASS', 'MarkdownExtra_Parser' ); + +function Markdown($text) { +# +# Initialize the parser and return the result of its transform method. +# + # Setup static parser variable. + static $parser; + if (!isset($parser)) { + $parser_class = MARKDOWN_PARSER_CLASS; + $parser = new $parser_class; + } + + # Transform text using parser. + return $parser->transform($text); +} + + +### WordPress Plugin Interface ### + +/* +Plugin Name: Markdown Extra +Plugin URI: http://www.michelf.com/projects/php-markdown/ +Description: Markdown syntax allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by John Gruber. More... +Version: 1.2.2 +Author: Michel Fortin +Author URI: http://www.michelf.com/ +*/ + +if (isset($wp_version)) { + # More details about how it works here: + # + + # Post content and excerpts + # - Remove WordPress paragraph generator. + # - Run Markdown on excerpt, then remove all tags. + # - Add paragraph tag around the excerpt, but remove it for the excerpt rss. + if (MARKDOWN_WP_POSTS) { + remove_filter('the_content', 'wpautop'); + remove_filter('the_content_rss', 'wpautop'); + remove_filter('the_excerpt', 'wpautop'); + add_filter('the_content', 'mdwp_MarkdownPost', 6); + add_filter('the_content_rss', 'mdwp_MarkdownPost', 6); + add_filter('get_the_excerpt', 'mdwp_MarkdownPost', 6); + add_filter('get_the_excerpt', 'trim', 7); + add_filter('the_excerpt', 'mdwp_add_p'); + add_filter('the_excerpt_rss', 'mdwp_strip_p'); + + remove_filter('content_save_pre', 'balanceTags', 50); + remove_filter('excerpt_save_pre', 'balanceTags', 50); + add_filter('the_content', 'balanceTags', 50); + add_filter('get_the_excerpt', 'balanceTags', 9); + } + + # Add a footnote id prefix to posts when inside a loop. + function mdwp_MarkdownPost($text) { + static $parser; + if (!$parser) { + $parser_class = MARKDOWN_PARSER_CLASS; + $parser = new $parser_class; + } + if (is_single() || is_page() || is_feed()) { + $parser->fn_id_prefix = ""; + } else { + $parser->fn_id_prefix = get_the_ID() . "."; + } + return $parser->transform($text); + } + + # Comments + # - Remove WordPress paragraph generator. + # - Remove WordPress auto-link generator. + # - Scramble important tags before passing them to the kses filter. + # - Run Markdown on excerpt then remove paragraph tags. + if (MARKDOWN_WP_COMMENTS) { + remove_filter('comment_text', 'wpautop', 30); + remove_filter('comment_text', 'make_clickable'); + add_filter('pre_comment_content', 'Markdown', 6); + add_filter('pre_comment_content', 'mdwp_hide_tags', 8); + add_filter('pre_comment_content', 'mdwp_show_tags', 12); + add_filter('get_comment_text', 'Markdown', 6); + add_filter('get_comment_excerpt', 'Markdown', 6); + add_filter('get_comment_excerpt', 'mdwp_strip_p', 7); + + global $mdwp_hidden_tags, $mdwp_placeholders; + $mdwp_hidden_tags = explode(' ', + '

             
          • '); + $mdwp_placeholders = explode(' ', str_rot13( + 'pEj07ZbbBZ U1kqgh4w4p pre2zmeN6K QTi31t9pre ol0MP1jzJR '. + 'ML5IjmbRol ulANi1NsGY J7zRLJqPul liA8ctl16T K9nhooUHli')); + } + + function mdwp_add_p($text) { + if (!preg_match('{^$|^<(p|ul|ol|dl|pre|blockquote)>}i', $text)) { + $text = '

            '.$text.'

            '; + $text = preg_replace('{\n{2,}}', "

            \n\n

            ", $text); + } + return $text; + } + + function mdwp_strip_p($t) { return preg_replace('{}i', '', $t); } + + function mdwp_hide_tags($text) { + global $mdwp_hidden_tags, $mdwp_placeholders; + return str_replace($mdwp_hidden_tags, $mdwp_placeholders, $text); + } + function mdwp_show_tags($text) { + global $mdwp_hidden_tags, $mdwp_placeholders; + return str_replace($mdwp_placeholders, $mdwp_hidden_tags, $text); + } +} + + +### bBlog Plugin Info ### + +function identify_modifier_markdown() { + return array( + 'name' => 'markdown', + 'type' => 'modifier', + 'nicename' => 'PHP Markdown Extra', + 'description' => 'A text-to-HTML conversion tool for web writers', + 'authors' => 'Michel Fortin and John Gruber', + 'licence' => 'GPL', + 'version' => MARKDOWNEXTRA_VERSION, + 'help' => 'Markdown syntax allows you to write using an easy-to-read, easy-to-write plain text format. Based on the original Perl version by John Gruber. More...', + ); +} + + +### Smarty Modifier Interface ### + +function smarty_modifier_markdown($text) { + return Markdown($text); +} + + +### Textile Compatibility Mode ### + +# Rename this file to "classTextile.php" and it can replace Textile everywhere. + +if (strcasecmp(substr(__FILE__, -16), "classTextile.php") == 0) { + # Try to include PHP SmartyPants. Should be in the same directory. + @include_once 'smartypants.php'; + # Fake Textile class. It calls Markdown instead. + class Textile { + function TextileThis($text, $lite='', $encode='') { + if ($lite == '' && $encode == '') $text = Markdown($text); + if (function_exists('SmartyPants')) $text = SmartyPants($text); + return $text; + } + # Fake restricted version: restrictions are not supported for now. + function TextileRestricted($text, $lite='', $noimage='') { + return $this->TextileThis($text, $lite); + } + # Workaround to ensure compatibility with TextPattern 4.0.3. + function blockLite($text) { return $text; } + } +} + + + +# +# Markdown Parser Class +# + +class Markdown_Parser { + + # Regex to match balanced [brackets]. + # Needed to insert a maximum bracked depth while converting to PHP. + var $nested_brackets_depth = 6; + var $nested_brackets_re; + + var $nested_url_parenthesis_depth = 4; + var $nested_url_parenthesis_re; + + # Table of hash values for escaped characters: + var $escape_chars = '\`*_{}[]()>#+-.!'; + var $escape_chars_re; + + # Change to ">" for HTML output. + var $empty_element_suffix = MARKDOWN_EMPTY_ELEMENT_SUFFIX; + var $tab_width = MARKDOWN_TAB_WIDTH; + + # Change to `true` to disallow markup or entities. + var $no_markup = false; + var $no_entities = false; + + # Predefined urls and titles for reference links and images. + var $predef_urls = array(); + var $predef_titles = array(); + + + function Markdown_Parser() { + # + # Constructor function. Initialize appropriate member variables. + # + $this->_initDetab(); + $this->prepareItalicsAndBold(); + + $this->nested_brackets_re = + str_repeat('(?>[^\[\]]+|\[', $this->nested_brackets_depth). + str_repeat('\])*', $this->nested_brackets_depth); + + $this->nested_url_parenthesis_re = + str_repeat('(?>[^()\s]+|\(', $this->nested_url_parenthesis_depth). + str_repeat('(?>\)))*', $this->nested_url_parenthesis_depth); + + $this->escape_chars_re = '['.preg_quote($this->escape_chars).']'; + + # Sort document, block, and span gamut in ascendent priority order. + asort($this->document_gamut); + asort($this->block_gamut); + asort($this->span_gamut); + } + + + # Internal hashes used during transformation. + var $urls = array(); + var $titles = array(); + var $html_hashes = array(); + + # Status flag to avoid invalid nesting. + var $in_anchor = false; + + + function setup() { + # + # Called before the transformation process starts to setup parser + # states. + # + # Clear global hashes. + $this->urls = $this->predef_urls; + $this->titles = $this->predef_titles; + $this->html_hashes = array(); + + $in_anchor = false; + } + + function teardown() { + # + # Called after the transformation process to clear any variable + # which may be taking up memory unnecessarly. + # + $this->urls = array(); + $this->titles = array(); + $this->html_hashes = array(); + } + + + function transform($text) { + # + # Main function. Performs some preprocessing on the input text + # and pass it through the document gamut. + # + $this->setup(); + + # Remove UTF-8 BOM and marker character in input, if present. + $text = preg_replace('{^\xEF\xBB\xBF|\x1A}', '', $text); + + # Standardize line endings: + # DOS to Unix and Mac to Unix + $text = preg_replace('{\r\n?}', "\n", $text); + + # Make sure $text ends with a couple of newlines: + $text .= "\n\n"; + + # Convert all tabs to spaces. + $text = $this->detab($text); + + # Turn block-level HTML blocks into hash entries + $text = $this->hashHTMLBlocks($text); + + # Strip any lines consisting only of spaces and tabs. + # This makes subsequent regexen easier to write, because we can + # match consecutive blank lines with /\n+/ instead of something + # contorted like /[ ]*\n+/ . + $text = preg_replace('/^[ ]+$/m', '', $text); + + # Run document gamut methods. + foreach ($this->document_gamut as $method => $priority) { + $text = $this->$method($text); + } + + $this->teardown(); + + return $text . "\n"; + } + + var $document_gamut = array( + # Strip link definitions, store in hashes. + "stripLinkDefinitions" => 20, + + "runBasicBlockGamut" => 30, + ); + + + function stripLinkDefinitions($text) { + # + # Strips link definitions from text, stores the URLs and titles in + # hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: ^[id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[(.+)\][ ]?: # id = $1 + [ ]* + \n? # maybe *one* newline + [ ]* + ? # url = $2 + [ ]* + \n? # maybe one newline + [ ]* + (?: + (?<=\s) # lookbehind for whitespace + ["(] + (.*?) # title = $3 + [")] + [ ]* + )? # title is optional + (?:\n+|\Z) + }xm', + array(&$this, '_stripLinkDefinitions_callback'), + $text); + return $text; + } + function _stripLinkDefinitions_callback($matches) { + $link_id = strtolower($matches[1]); + $this->urls[$link_id] = $matches[2]; + $this->titles[$link_id] =& $matches[3]; + return ''; # String that will replace the block + } + + + function hashHTMLBlocks($text) { + if ($this->no_markup) return $text; + + $less_than_tab = $this->tab_width - 1; + + # Hashify HTML blocks: + # We only want to do this for block-level HTML tags, such as headers, + # lists, and tables. That's because we still want to wrap

            s around + # "paragraphs" that are wrapped in non-block-level tags, such as anchors, + # phrase emphasis, and spans. The list of tags we're looking for is + # hard-coded: + # + # * List "a" is made of tags which can be both inline or block-level. + # These will be treated block-level when the start tag is alone on + # its line, otherwise they're not matched here and will be taken as + # inline later. + # * List "b" is made of tags which are always block-level; + # + $block_tags_a_re = 'ins|del'; + $block_tags_b_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|'. + 'script|noscript|form|fieldset|iframe|math'; + + # Regular expression for the content of a block tag. + $nested_tags_level = 4; + $attr = ' + (?> # optional tag attributes + \s # starts with whitespace + (?> + [^>"/]+ # text outside quotes + | + /+(?!>) # slash not followed by ">" + | + "[^"]*" # text inside double quotes (tolerate ">") + | + \'[^\']*\' # text inside single quotes (tolerate ">") + )* + )? + '; + $content = + str_repeat(' + (?> + [^<]+ # content without tag + | + <\2 # nested opening tag + '.$attr.' # attributes + (?> + /> + | + >', $nested_tags_level). # end of opening tag + '.*?'. # last level nested tag content + str_repeat(' + # closing nested tag + ) + | + <(?!/\2\s*> # other tags with a different name + ) + )*', + $nested_tags_level); + $content2 = str_replace('\2', '\3', $content); + + # First, look for nested blocks, e.g.: + #

            + #
            + # tags for inner block must be indented. + #
            + #
            + # + # The outermost tags must start at the left margin for this to match, and + # the inner nested divs must be indented. + # We need to do this before the next, more liberal match, because the next + # match will start at the first `
            ` and stop at the first `
            `. + $text = preg_replace_callback('{(?> + (?> + (?<=\n\n) # Starting after a blank line + | # or + \A\n? # the beginning of the doc + ) + ( # save in $1 + + # Match from `\n` to `\n`, handling nested tags + # in between. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_b_re.')# start tag = $2 + '.$attr.'> # attributes followed by > and \n + '.$content.' # content, support nesting + # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special version for tags of group a. + + [ ]{0,'.$less_than_tab.'} + <('.$block_tags_a_re.')# start tag = $3 + '.$attr.'>[ ]*\n # attributes followed by > + '.$content2.' # content, support nesting + # the matching end tag + [ ]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + + | # Special case just for
            . It was easier to make a special + # case than to make the other regex more complicated. + + [ ]{0,'.$less_than_tab.'} + <(hr) # start tag = $2 + '.$attr.' # attributes + /?> # the matching end tag + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # Special case for standalone HTML comments: + + [ ]{0,'.$less_than_tab.'} + (?s: + + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + | # PHP and ASP-style processor instructions ( + ) + [ ]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + + ) + )}Sxmi', + array(&$this, '_hashHTMLBlocks_callback'), + $text); + + return $text; + } + function _hashHTMLBlocks_callback($matches) { + $text = $matches[1]; + $key = $this->hashBlock($text); + return "\n\n$key\n\n"; + } + + + function hashPart($text, $boundary = 'X') { + # + # Called whenever a tag must be hashed when a function insert an atomic + # element in the text stream. Passing $text to through this function gives + # a unique text-token which will be reverted back when calling unhash. + # + # The $boundary argument specify what character should be used to surround + # the token. By convension, "B" is used for block elements that needs not + # to be wrapped into paragraph tags at the end, ":" is used for elements + # that are word separators and "X" is used in the general case. + # + # Swap back any tag hash found in $text so we do not have to `unhash` + # multiple times at the end. + $text = $this->unhash($text); + + # Then hash the block. + static $i = 0; + $key = "$boundary\x1A" . ++$i . $boundary; + $this->html_hashes[$key] = $text; + return $key; # String that will replace the tag. + } + + + function hashBlock($text) { + # + # Shortcut function for hashPart with block-level boundaries. + # + return $this->hashPart($text, 'B'); + } + + + var $block_gamut = array( + # + # These are all the transformations that form block-level + # tags like paragraphs, headers, and list items. + # + "doHeaders" => 10, + "doHorizontalRules" => 20, + + "doLists" => 40, + "doCodeBlocks" => 50, + "doBlockQuotes" => 60, + ); + + function runBlockGamut($text) { + # + # Run block gamut tranformations. + # + # We need to escape raw HTML in Markdown source before doing anything + # else. This need to be done for each block, and not only at the + # begining in the Markdown function since hashed blocks can be part of + # list items and could have been indented. Indented blocks would have + # been seen as a code block in a previous pass of hashHTMLBlocks. + $text = $this->hashHTMLBlocks($text); + + return $this->runBasicBlockGamut($text); + } + + function runBasicBlockGamut($text) { + # + # Run block gamut tranformations, without hashing HTML blocks. This is + # useful when HTML blocks are known to be already hashed, like in the first + # whole-document pass. + # + foreach ($this->block_gamut as $method => $priority) { + $text = $this->$method($text); + } + + # Finally form paragraph and restore hashed blocks. + $text = $this->formParagraphs($text); + + return $text; + } + + + function doHorizontalRules($text) { + # Do Horizontal Rules: + return preg_replace( + '{ + ^[ ]{0,3} # Leading space + ([-*_]) # $1: First marker + (?> # Repeated marker group + [ ]{0,2} # Zero, one, or two spaces. + \1 # Marker character + ){2,} # Group repeated at least twice + [ ]* # Tailing spaces + $ # End of line. + }mx', + "\n".$this->hashBlock("empty_element_suffix")."\n", + $text); + } + + + var $span_gamut = array( + # + # These are all the transformations that occur *within* block-level + # tags like paragraphs, headers, and list items. + # + # Process character escapes, code spans, and inline HTML + # in one shot. + "parseSpan" => -30, + + # Process anchor and image tags. Images must come first, + # because ![foo][f] looks like an anchor. + "doImages" => 10, + "doAnchors" => 20, + + # Make links out of things like `` + # Must come after doAnchors, because you can use < and > + # delimiters in inline links like [this](). + "doAutoLinks" => 30, + "encodeAmpsAndAngles" => 40, + + "doItalicsAndBold" => 50, + "doHardBreaks" => 60, + ); + + function runSpanGamut($text) { + # + # Run span gamut tranformations. + # + foreach ($this->span_gamut as $method => $priority) { + $text = $this->$method($text); + } + + return $text; + } + + + function doHardBreaks($text) { + # Do hard breaks: + return preg_replace_callback('/ {2,}\n/', + array(&$this, '_doHardBreaks_callback'), $text); + } + function _doHardBreaks_callback($matches) { + return $this->hashPart("empty_element_suffix\n"); + } + + + function doAnchors($text) { + # + # Turn Markdown link shortcuts into XHTML tags. + # + if ($this->in_anchor) return $text; + $this->in_anchor = true; + + # + # First, handle reference-style links: [link text] [id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + ) + }xs', + array(&$this, '_doAnchors_reference_callback'), $text); + + # + # Next, inline-style links: [link text](url "optional title") + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + \[ + ('.$this->nested_brackets_re.') # link text = $2 + \] + \( # literal paren + [ ]* + (?: + <(\S*)> # href = $3 + | + ('.$this->nested_url_parenthesis_re.') # href = $4 + ) + [ ]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # Title = $7 + \6 # matching quote + [ ]* # ignore any spaces/tabs between closing quote and ) + )? # title is optional + \) + ) + }xs', + array(&$this, '_DoAnchors_inline_callback'), $text); + + # + # Last, handle reference-style shortcuts: [link text] + # These must come last in case you've also got [link test][1] + # or [link test](/foo) + # +// $text = preg_replace_callback('{ +// ( # wrap whole match in $1 +// \[ +// ([^\[\]]+) # link text = $2; can\'t contain [ or ] +// \] +// ) +// }xs', +// array(&$this, '_doAnchors_reference_callback'), $text); + + $this->in_anchor = false; + return $text; + } + function _doAnchors_reference_callback($matches) { + $whole_match = $matches[1]; + $link_text = $matches[2]; + $link_id =& $matches[3]; + + if ($link_id == "") { + # for shortcut links like [this][] or [this]. + $link_id = $link_text; + } + + # lower-case and turn embedded newlines into spaces + $link_id = strtolower($link_id); + $link_id = preg_replace('{[ ]?\n}', ' ', $link_id); + + if (isset($this->urls[$link_id])) { + $url = $this->urls[$link_id]; + $url = $this->encodeAttribute($url); + + $result = "titles[$link_id] ) ) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text"; + $result = $this->hashPart($result); + } + else { + $result = $whole_match; + } + return $result; + } + function _doAnchors_inline_callback($matches) { + $whole_match = $matches[1]; + $link_text = $this->runSpanGamut($matches[2]); + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + $url = $this->encodeAttribute($url); + + $result = "encodeAttribute($title); + $result .= " title=\"$title\""; + } + + $link_text = $this->runSpanGamut($link_text); + $result .= ">$link_text"; + + return $this->hashPart($result); + } + + + function doImages($text) { + # + # Turn Markdown image shortcuts into tags. + # + # + # First, handle reference-style labeled images: ![alt text][id] + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + + \[ + (.*?) # id = $3 + \] + + ) + }xs', + array(&$this, '_doImages_reference_callback'), $text); + + # + # Next, handle inline images: ![alt text](url "optional title") + # Don't forget: encode * and _ + # + $text = preg_replace_callback('{ + ( # wrap whole match in $1 + !\[ + ('.$this->nested_brackets_re.') # alt text = $2 + \] + \s? # One optional whitespace character + \( # literal paren + [ ]* + (?: + <(\S*)> # src url = $3 + | + ('.$this->nested_url_parenthesis_re.') # src url = $4 + ) + [ ]* + ( # $5 + ([\'"]) # quote char = $6 + (.*?) # title = $7 + \6 # matching quote + [ ]* + )? # title is optional + \) + ) + }xs', + array(&$this, '_doImages_inline_callback'), $text); + + return $text; + } + function _doImages_reference_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $link_id = strtolower($matches[3]); + + if ($link_id == "") { + $link_id = strtolower($alt_text); # for shortcut links like ![this][]. + } + + $alt_text = $this->encodeAttribute($alt_text); + if (isset($this->urls[$link_id])) { + $url = $this->encodeAttribute($this->urls[$link_id]); + $result = "\"$alt_text\"";titles[$link_id])) { + $title = $this->titles[$link_id]; + $title = $this->encodeAttribute($title); + $result .= " title=\"$title\""; + } + $result .= $this->empty_element_suffix; + $result = $this->hashPart($result); + } + else { + # If there's no such link ID, leave intact: + $result = $whole_match; + } + + return $result; + } + function _doImages_inline_callback($matches) { + $whole_match = $matches[1]; + $alt_text = $matches[2]; + $url = $matches[3] == '' ? $matches[4] : $matches[3]; + $title =& $matches[7]; + + $alt_text = $this->encodeAttribute($alt_text); + $url = $this->encodeAttribute($url); + $result = "\"$alt_text\"";encodeAttribute($title); + $result .= " title=\"$title\""; # $title already quoted + } + $result .= $this->empty_element_suffix; + + return $this->hashPart($result); + } + + + function doHeaders($text) { + # Setext-style headers: + # Header 1 + # ======== + # + # Header 2 + # -------- + # + $text = preg_replace_callback('{ ^(.+?)[ ]*\n(=+|-+)[ ]*\n+ }mx', + array(&$this, '_doHeaders_callback_setext'), $text); + + # atx-style headers: + # # Header 1 + # ## Header 2 + # ## Header 2 with closing hashes ## + # ... + # ###### Header 6 + # + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + \n+ + }xm', + array(&$this, '_doHeaders_callback_atx'), $text); + + return $text; + } + function _doHeaders_callback_setext($matches) { + # Terrible hack to check we haven't found an empty list item. + if ($matches[2] == '-' && preg_match('{^-(?: |$)}', $matches[1])) + return $matches[0]; + + $level = $matches[2]{0} == '=' ? 1 : 2; + $block = "".$this->runSpanGamut($matches[1]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + function _doHeaders_callback_atx($matches) { + $level = strlen($matches[1]); + $block = "".$this->runSpanGamut($matches[2]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + + function doLists($text) { + # + # Form HTML ordered (numbered) and unordered (bulleted) lists. + # + $less_than_tab = $this->tab_width - 1; + + # Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[.]'; + $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + + $markers_relist = array($marker_ul_re, $marker_ol_re); + + foreach ($markers_relist as $marker_re) { + # Re-usable pattern to match any entirel ul or ol list: + $whole_list_re = ' + ( # $1 = whole list + ( # $2 + [ ]{0,'.$less_than_tab.'} + ('.$marker_re.') # $3 = first list item marker + [ ]+ + ) + (?s:.+?) + ( # $4 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another list item marker + [ ]* + '.$marker_re.'[ ]+ + ) + ) + ) + '; // mx + + # We use a different prefix before nested lists than top-level lists. + # See extended comment in _ProcessListItems(). + + if ($this->list_level) { + $text = preg_replace_callback('{ + ^ + '.$whole_list_re.' + }mx', + array(&$this, '_doLists_callback'), $text); + } + else { + $text = preg_replace_callback('{ + (?:(?<=\n)\n|\A\n?) # Must eat the newline + '.$whole_list_re.' + }mx', + array(&$this, '_doLists_callback'), $text); + } + } + + return $text; + } + function _doLists_callback($matches) { + # Re-usable patterns to match list item bullets and number markers: + $marker_ul_re = '[*+-]'; + $marker_ol_re = '\d+[.]'; + $marker_any_re = "(?:$marker_ul_re|$marker_ol_re)"; + + $list = $matches[1]; + $list_type = preg_match("/$marker_ul_re/", $matches[3]) ? "ul" : "ol"; + + $marker_any_re = ( $list_type == "ul" ? $marker_ul_re : $marker_ol_re ); + + $list .= "\n"; + $result = $this->processListItems($list, $marker_any_re); + + $result = $this->hashBlock("<$list_type>\n" . $result . ""); + return "\n". $result ."\n\n"; + } + + var $list_level = 0; + + function processListItems($list_str, $marker_any_re) { + # + # Process the contents of a single ordered or unordered list, splitting it + # into individual list items. + # + # The $this->list_level global keeps track of when we're inside a list. + # Each time we enter a list, we increment it; when we leave a list, + # we decrement. If it's zero, we're not in a list anymore. + # + # We do this because when we're not inside a list, we want to treat + # something like this: + # + # I recommend upgrading to version + # 8. Oops, now this line is treated + # as a sub-list. + # + # As a single paragraph, despite the fact that the second line starts + # with a digit-period-space sequence. + # + # Whereas when we're inside a list (or sub-list), that line will be + # treated as the start of a sub-list. What a kludge, huh? This is + # an aspect of Markdown's syntax that's hard to parse perfectly + # without resorting to mind-reading. Perhaps the solution is to + # change the syntax rules such that sub-lists must start with a + # starting cardinal number; e.g. "1." or "a.". + + $this->list_level++; + + # trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + $list_str = preg_replace_callback('{ + (\n)? # leading line = $1 + (^[ ]*) # leading whitespace = $2 + ('.$marker_any_re.' # list marker and space = $3 + (?:[ ]+|(?=\n)) # space only required if item is not empty + ) + ((?s:.*?)) # list item text = $4 + (?:(\n+(?=\n))|\n) # tailing blank line = $5 + (?= \n* (\z | \2 ('.$marker_any_re.') (?:[ ]+|(?=\n)))) + }xm', + array(&$this, '_processListItems_callback'), $list_str); + + $this->list_level--; + return $list_str; + } + function _processListItems_callback($matches) { + $item = $matches[4]; + $leading_line =& $matches[1]; + $leading_space =& $matches[2]; + $marker_space = $matches[3]; + $tailing_blank_line =& $matches[5]; + + if ($leading_line || $tailing_blank_line || + preg_match('/\n{2,}/', $item)) + { + # Replace marker with the appropriate whitespace indentation + $item = $leading_space . str_repeat(' ', strlen($marker_space)) . $item; + $item = $this->runBlockGamut($this->outdent($item)."\n"); + } + else { + # Recursion for sub-lists: + $item = $this->doLists($this->outdent($item)); + $item = preg_replace('/\n+$/', '', $item); + $item = $this->runSpanGamut($item); + } + + return "
          • " . $item . "
          • \n"; + } + + + function doCodeBlocks($text) { + # + # Process Markdown `
            ` blocks.
            +	#
            +		$text = preg_replace_callback('{
            +				(?:\n\n|\A\n?)
            +				(	            # $1 = the code block -- one or more lines, starting with a space/tab
            +				  (?>
            +					[ ]{'.$this->tab_width.'}  # Lines must start with a tab or a tab-width of spaces
            +					.*\n+
            +				  )+
            +				)
            +				((?=^[ ]{0,'.$this->tab_width.'}\S)|\Z)	# Lookahead for non-space at line-start, or end of doc
            +			}xm',
            +			array(&$this, '_doCodeBlocks_callback'), $text);
            +
            +		return $text;
            +	}
            +	function _doCodeBlocks_callback($matches) {
            +		$codeblock = $matches[1];
            +
            +		$codeblock = $this->outdent($codeblock);
            +		$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES);
            +
            +		# trim leading newlines and trailing newlines
            +		$codeblock = preg_replace('/\A\n+|\n+\z/', '', $codeblock);
            +
            +		$codeblock = "
            $codeblock\n
            "; + return "\n\n".$this->hashBlock($codeblock)."\n\n"; + } + + + function makeCodeSpan($code) { + # + # Create a code span markup for $code. Called from handleSpanToken. + # + $code = htmlspecialchars(trim($code), ENT_NOQUOTES); + return $this->hashPart("$code"); + } + + + var $em_relist = array( + '' => '(?:(? '(?<=\S)(? '(?<=\S)(? '(?:(? '(?<=\S)(? '(?<=\S)(? '(?:(? '(?<=\S)(? '(?<=\S)(?em_relist as $em => $em_re) { + foreach ($this->strong_relist as $strong => $strong_re) { + # Construct list of allowed token expressions. + $token_relist = array(); + if (isset($this->em_strong_relist["$em$strong"])) { + $token_relist[] = $this->em_strong_relist["$em$strong"]; + } + $token_relist[] = $em_re; + $token_relist[] = $strong_re; + + # Construct master expression from list. + $token_re = '{('. implode('|', $token_relist) .')}'; + $this->em_strong_prepared_relist["$em$strong"] = $token_re; + } + } + } + + function doItalicsAndBold($text) { + $token_stack = array(''); + $text_stack = array(''); + $em = ''; + $strong = ''; + $tree_char_em = false; + + while (1) { + # + # Get prepared regular expression for seraching emphasis tokens + # in current context. + # + $token_re = $this->em_strong_prepared_relist["$em$strong"]; + + # + # Each loop iteration seach for the next emphasis token. + # Each token is then passed to handleSpanToken. + # + $parts = preg_split($token_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + $text_stack[0] .= $parts[0]; + $token =& $parts[1]; + $text =& $parts[2]; + + if (empty($token)) { + # Reached end of text span: empty stack without emitting. + # any more emphasis. + while ($token_stack[0]) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + } + break; + } + + $token_len = strlen($token); + if ($tree_char_em) { + # Reached closing marker while inside a three-char emphasis. + if ($token_len == 3) { + # Three-char closing marker, close em and strong. + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "$span"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + $strong = ''; + } else { + # Other closing marker: close one em or strong and + # change current token state to match the other + $token_stack[0] = str_repeat($token{0}, 3-$token_len); + $tag = $token_len == 2 ? "strong" : "em"; + $span = $text_stack[0]; + $span = $this->runSpanGamut($span); + $span = "<$tag>$span"; + $text_stack[0] = $this->hashPart($span); + $$tag = ''; # $$tag stands for $em or $strong + } + $tree_char_em = false; + } else if ($token_len == 3) { + if ($em) { + # Reached closing marker for both em and strong. + # Closing strong marker: + for ($i = 0; $i < 2; ++$i) { + $shifted_token = array_shift($token_stack); + $tag = strlen($shifted_token) == 2 ? "strong" : "em"; + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "<$tag>$span"; + $text_stack[0] .= $this->hashPart($span); + $$tag = ''; # $$tag stands for $em or $strong + } + } else { + # Reached opening three-char emphasis marker. Push on token + # stack; will be handled by the special condition above. + $em = $token{0}; + $strong = "$em$em"; + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $tree_char_em = true; + } + } else if ($token_len == 2) { + if ($strong) { + # Unwind any dangling emphasis marker: + if (strlen($token_stack[0]) == 1) { + $text_stack[1] .= array_shift($token_stack); + $text_stack[0] .= array_shift($text_stack); + } + # Closing strong marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "$span"; + $text_stack[0] .= $this->hashPart($span); + $strong = ''; + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $strong = $token; + } + } else { + # Here $token_len == 1 + if ($em) { + if (strlen($token_stack[0]) == 1) { + # Closing emphasis marker: + array_shift($token_stack); + $span = array_shift($text_stack); + $span = $this->runSpanGamut($span); + $span = "$span"; + $text_stack[0] .= $this->hashPart($span); + $em = ''; + } else { + $text_stack[0] .= $token; + } + } else { + array_unshift($token_stack, $token); + array_unshift($text_stack, ''); + $em = $token; + } + } + } + return $text_stack[0]; + } + + + function doBlockQuotes($text) { + $text = preg_replace_callback('/ + ( # Wrap whole match in $1 + (?> + ^[ ]*>[ ]? # ">" at the start of a line + .+\n # rest of the first line + (.+\n)* # subsequent consecutive lines + \n* # blanks + )+ + ) + /xm', + array(&$this, '_doBlockQuotes_callback'), $text); + + return $text; + } + function _doBlockQuotes_callback($matches) { + $bq = $matches[1]; + # trim one level of quoting - trim whitespace-only lines + $bq = preg_replace('/^[ ]*>[ ]?|^[ ]+$/m', '', $bq); + $bq = $this->runBlockGamut($bq); # recurse + + $bq = preg_replace('/^/m', " ", $bq); + # These leading spaces cause problem with
             content, 
            +		# so we need to fix that:
            +		$bq = preg_replace_callback('{(\s*
            .+?
            )}sx', + array(&$this, '_DoBlockQuotes_callback2'), $bq); + + return "\n". $this->hashBlock("
            \n$bq\n
            ")."\n\n"; + } + function _doBlockQuotes_callback2($matches) { + $pre = $matches[1]; + $pre = preg_replace('/^ /m', '', $pre); + return $pre; + } + + + function formParagraphs($text) { + # + # Params: + # $text - string to process with html

            tags + # + # Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + # + # Wrap

            tags and unhashify HTML blocks + # + foreach ($grafs as $key => $value) { + if (!preg_match('/^B\x1A[0-9]+B$/', $value)) { + # Is a paragraph. + $value = $this->runSpanGamut($value); + $value = preg_replace('/^([ ]*)/', "

            ", $value); + $value .= "

            "; + $grafs[$key] = $this->unhash($value); + } + else { + # Is a block. + # Modify elements of @grafs in-place... + $graf = $value; + $block = $this->html_hashes[$graf]; + $graf = $block; +// if (preg_match('{ +// \A +// ( # $1 =
            tag +//
            ]* +// \b +// markdown\s*=\s* ([\'"]) # $2 = attr quote char +// 1 +// \2 +// [^>]* +// > +// ) +// ( # $3 = contents +// .* +// ) +// (
            ) # $4 = closing tag +// \z +// }xs', $block, $matches)) +// { +// list(, $div_open, , $div_content, $div_close) = $matches; +// +// # We can't call Markdown(), because that resets the hash; +// # that initialization code should be pulled into its own sub, though. +// $div_content = $this->hashHTMLBlocks($div_content); +// +// # Run document gamut methods on the content. +// foreach ($this->document_gamut as $method => $priority) { +// $div_content = $this->$method($div_content); +// } +// +// $div_open = preg_replace( +// '{\smarkdown\s*=\s*([\'"]).+?\1}', '', $div_open); +// +// $graf = $div_open . "\n" . $div_content . "\n" . $div_close; +// } + $grafs[$key] = $graf; + } + } + + return implode("\n\n", $grafs); + } + + + function encodeAttribute($text) { + # + # Encode text for a double-quoted HTML attribute. This function + # is *not* suitable for attributes enclosed in single quotes. + # + $text = $this->encodeAmpsAndAngles($text); + $text = str_replace('"', '"', $text); + return $text; + } + + + function encodeAmpsAndAngles($text) { + # + # Smart processing for ampersands and angle brackets that need to + # be encoded. Valid character entities are left alone unless the + # no-entities mode is set. + # + if ($this->no_entities) { + $text = str_replace('&', '&', $text); + } else { + # Ampersand-encoding based entirely on Nat Irons's Amputator + # MT plugin: + $text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', + '&', $text);; + } + # Encode remaining <'s + $text = str_replace('<', '<', $text); + + return $text; + } + + + function doAutoLinks($text) { + $text = preg_replace_callback('{<((https?|ftp|dict):[^\'">\s]+)>}i', + array(&$this, '_doAutoLinks_url_callback'), $text); + + # Email addresses: + $text = preg_replace_callback('{ + < + (?:mailto:)? + ( + [-.\w\x80-\xFF]+ + \@ + [-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+ + ) + > + }xi', + array(&$this, '_doAutoLinks_email_callback'), $text); + + return $text; + } + function _doAutoLinks_url_callback($matches) { + $url = $this->encodeAttribute($matches[1]); + $link = "$url"; + return $this->hashPart($link); + } + function _doAutoLinks_email_callback($matches) { + $address = $matches[1]; + $link = $this->encodeEmailAddress($address); + return $this->hashPart($link); + } + + + function encodeEmailAddress($addr) { + # + # Input: an email address, e.g. "foo@example.com" + # + # Output: the email address as a mailto link, with each character + # of the address encoded as either a decimal or hex entity, in + # the hopes of foiling most address harvesting spam bots. E.g.: + # + #

            foo@exampl + # e.com

            + # + # Based by a filter by Matthew Wickline, posted to BBEdit-Talk. + # With some optimizations by Milian Wolff. + # + $addr = "mailto:" . $addr; + $chars = preg_split('/(? $char) { + $ord = ord($char); + # Ignore non-ascii chars. + if ($ord < 128) { + $r = ($seed * (1 + $key)) % 100; # Pseudo-random function. + # roughly 10% raw, 45% hex, 45% dec + # '@' *must* be encoded. I insist. + if ($r > 90 && $char != '@') /* do nothing */; + else if ($r < 45) $chars[$key] = '&#x'.dechex($ord).';'; + else $chars[$key] = '&#'.$ord.';'; + } + } + + $addr = implode('', $chars); + $text = implode('', array_slice($chars, 7)); # text without `mailto:` + $addr = "$text"; + + return $addr; + } + + + function parseSpan($str) { + # + # Take the string $str and parse it into tokens, hashing embeded HTML, + # escaped characters and handling code spans. + # + $output = ''; + + $span_re = '{ + ( + \\\\'.$this->escape_chars_re.' + | + (?no_markup ? '' : ' + | + # comment + | + <\?.*?\?> | <%.*?%> # processing instruction + | + <[/!$]?[-a-zA-Z0-9:]+ # regular tags + (?> + \s + (?>[^"\'>]+|"[^"]*"|\'[^\']*\')* + )? + > + ').' + ) + }xs'; + + while (1) { + # + # Each loop iteration seach for either the next tag, the next + # openning code span marker, or the next escaped character. + # Each token is then passed to handleSpanToken. + # + $parts = preg_split($span_re, $str, 2, PREG_SPLIT_DELIM_CAPTURE); + + # Create token from text preceding tag. + if ($parts[0] != "") { + $output .= $parts[0]; + } + + # Check if we reach the end. + if (isset($parts[1])) { + $output .= $this->handleSpanToken($parts[1], $parts[2]); + $str = $parts[2]; + } + else { + break; + } + } + + return $output; + } + + + function handleSpanToken($token, &$str) { + # + # Handle $token provided by parseSpan by determining its nature and + # returning the corresponding value that should replace it. + # + switch ($token{0}) { + case "\\": + return $this->hashPart("&#". ord($token{1}). ";"); + case "`": + # Search for end marker in remaining text. + if (preg_match('/^(.*?[^`])'.preg_quote($token).'(?!`)(.*)$/sm', + $str, $matches)) + { + $str = $matches[2]; + $codespan = $this->makeCodeSpan($matches[1]); + return $this->hashPart($codespan); + } + return $token; // return as text since no ending marker found. + default: + return $this->hashPart($token); + } + } + + + function outdent($text) { + # + # Remove one level of line-leading tabs or spaces + # + return preg_replace('/^(\t|[ ]{1,'.$this->tab_width.'})/m', '', $text); + } + + + # String length function for detab. `_initDetab` will create a function to + # hanlde UTF-8 if the default function does not exist. + var $utf8_strlen = 'mb_strlen'; + + function detab($text) { + # + # Replace tabs with the appropriate amount of space. + # + # For each line we separate the line in blocks delemited by + # tab characters. Then we reconstruct every line by adding the + # appropriate number of space between each blocks. + + $text = preg_replace_callback('/^.*\t.*$/m', + array(&$this, '_detab_callback'), $text); + + return $text; + } + function _detab_callback($matches) { + $line = $matches[0]; + $strlen = $this->utf8_strlen; # strlen function for UTF-8. + + # Split in blocks. + $blocks = explode("\t", $line); + # Add each blocks to the line. + $line = $blocks[0]; + unset($blocks[0]); # Do not add first block twice. + foreach ($blocks as $block) { + # Calculate amount of space, insert spaces, insert block. + $amount = $this->tab_width - + $strlen($line, 'UTF-8') % $this->tab_width; + $line .= str_repeat(" ", $amount) . $block; + } + return $line; + } + function _initDetab() { + # + # Check for the availability of the function in the `utf8_strlen` property + # (initially `mb_strlen`). If the function is not available, create a + # function that will loosely count the number of UTF-8 characters with a + # regular expression. + # + if (function_exists($this->utf8_strlen)) return; + $this->utf8_strlen = create_function('$text', 'return preg_match_all( + "/[\\\\x00-\\\\xBF]|[\\\\xC0-\\\\xFF][\\\\x80-\\\\xBF]*/", + $text, $m);'); + } + + + function unhash($text) { + # + # Swap back in all the tags hashed by _HashHTMLBlocks. + # + return preg_replace_callback('/(.)\x1A[0-9]+\1/', + array(&$this, '_unhash_callback'), $text); + } + function _unhash_callback($matches) { + return $this->html_hashes[$matches[0]]; + } + +} + + +# +# Markdown Extra Parser Class +# + +class MarkdownExtra_Parser extends Markdown_Parser { + + # Prefix for footnote ids. + var $fn_id_prefix = ""; + + # Optional title attribute for footnote links and backlinks. + var $fn_link_title = MARKDOWN_FN_LINK_TITLE; + var $fn_backlink_title = MARKDOWN_FN_BACKLINK_TITLE; + + # Optional class attribute for footnote links and backlinks. + var $fn_link_class = MARKDOWN_FN_LINK_CLASS; + var $fn_backlink_class = MARKDOWN_FN_BACKLINK_CLASS; + + # Predefined abbreviations. + var $predef_abbr = array(); + + + function MarkdownExtra_Parser() { + # + # Constructor function. Initialize the parser object. + # + # Add extra escapable characters before parent constructor + # initialize the table. + $this->escape_chars .= ':|'; + + # Insert extra document, block, and span transformations. + # Parent constructor will do the sorting. + $this->document_gamut += array( + "doFencedCodeBlocks" => 5, + "stripFootnotes" => 15, + "stripAbbreviations" => 25, + "appendFootnotes" => 50, + ); + $this->block_gamut += array( + "doFencedCodeBlocks" => 5, + "doTables" => 15, + "doDefLists" => 45, + ); + $this->span_gamut += array( + "doFootnotes" => 5, + "doAbbreviations" => 70, + ); + + parent::Markdown_Parser(); + } + + + # Extra variables used during extra transformations. + var $footnotes = array(); + var $footnotes_ordered = array(); + var $abbr_desciptions = array(); + var $abbr_word_re = ''; + + # Give the current footnote number. + var $footnote_counter = 1; + + + function setup() { + # + # Setting up Extra-specific variables. + # + parent::setup(); + + $this->footnotes = array(); + $this->footnotes_ordered = array(); + $this->abbr_desciptions = array(); + $this->abbr_word_re = ''; + $this->footnote_counter = 1; + + foreach ($this->predef_abbr as $abbr_word => $abbr_desc) { + if ($this->abbr_word_re) + $this->abbr_word_re .= '|'; + $this->abbr_word_re .= preg_quote($abbr_word); + $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); + } + } + + function teardown() { + # + # Clearing Extra-specific variables. + # + $this->footnotes = array(); + $this->footnotes_ordered = array(); + $this->abbr_desciptions = array(); + $this->abbr_word_re = ''; + + parent::teardown(); + } + + + ### HTML Block Parser ### + + # Tags that are always treated as block tags: + var $block_tags_re = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|form|fieldset|iframe|hr|legend'; + + # Tags treated as block tags only if the opening tag is alone on it's line: + var $context_block_tags_re = 'script|noscript|math|ins|del'; + + # Tags where markdown="1" default to span mode: + var $contain_span_tags_re = 'p|h[1-6]|li|dd|dt|td|th|legend|address'; + + # Tags which must not have their contents modified, no matter where + # they appear: + var $clean_tags_re = 'script|math'; + + # Tags that do not need to be closed. + var $auto_close_tags_re = 'hr|img'; + + + function hashHTMLBlocks($text) { + # + # Hashify HTML Blocks and "clean tags". + # + # We only want to do this for block-level HTML tags, such as headers, + # lists, and tables. That's because we still want to wrap

            s around + # "paragraphs" that are wrapped in non-block-level tags, such as anchors, + # phrase emphasis, and spans. The list of tags we're looking for is + # hard-coded. + # + # This works by calling _HashHTMLBlocks_InMarkdown, which then calls + # _HashHTMLBlocks_InHTML when it encounter block tags. When the markdown="1" + # attribute is found whitin a tag, _HashHTMLBlocks_InHTML calls back + # _HashHTMLBlocks_InMarkdown to handle the Markdown syntax within the tag. + # These two functions are calling each other. It's recursive! + # + # + # Call the HTML-in-Markdown hasher. + # + list($text, ) = $this->_hashHTMLBlocks_inMarkdown($text); + + return $text; + } + function _hashHTMLBlocks_inMarkdown($text, $indent = 0, + $enclosing_tag_re = '', $span = false) + { + # + # Parse markdown text, calling _HashHTMLBlocks_InHTML for block tags. + # + # * $indent is the number of space to be ignored when checking for code + # blocks. This is important because if we don't take the indent into + # account, something like this (which looks right) won't work as expected: + # + #

            + #
            + # Hello World. <-- Is this a Markdown code block or text? + #
            <-- Is this a Markdown code block or a real tag? + #
            + # + # If you don't like this, just don't indent the tag on which + # you apply the markdown="1" attribute. + # + # * If $enclosing_tag_re is not empty, stops at the first unmatched closing + # tag with that name. Nested tags supported. + # + # * If $span is true, text inside must treated as span. So any double + # newline will be replaced by a single newline so that it does not create + # paragraphs. + # + # Returns an array of that form: ( processed text , remaining text ) + # + if ($text === '') return array('', ''); + + # Regex to check for the presense of newlines around a block tag. + $newline_before_re = '/(?:^\n?|\n\n)*$/'; + $newline_after_re = + '{ + ^ # Start of text following the tag. + (?>[ ]*)? # Optional comment. + [ ]*\n # Must be followed by newline. + }xs'; + + # Regex to match any tag. + $block_tag_re = + '{ + ( # $2: Capture hole tag. + # Tag name. + '.$this->block_tags_re.' | + '.$this->context_block_tags_re.' | + '.$this->clean_tags_re.' | + (?!\s)'.$enclosing_tag_re.' + ) + (?: + (?=[\s"\'/a-zA-Z0-9]) # Allowed characters after tag name. + (?> + ".*?" | # Double quotes (can contain `>`) + \'.*?\' | # Single quotes (can contain `>`) + .+? # Anything but quotes and `>`. + )*? + )? + > # End of tag. + | + # HTML Comment + | + <\?.*?\?> | <%.*?%> # Processing instruction + | + # CData Block + | + # Code span marker + `+ + '. ( !$span ? ' # If not in span. + | + # Indented code block + (?> ^[ ]*\n? | \n[ ]*\n ) + [ ]{'.($indent+4).'}[^\n]* \n + (?> + (?: [ ]{'.($indent+4).'}[^\n]* | [ ]* ) \n + )* + | + # Fenced code block marker + (?> ^ | \n ) + [ ]{'.($indent).'}~~~+[ ]*\n + ' : '' ). ' # End (if not is span). + ) + }xs'; + + + $depth = 0; # Current depth inside the tag tree. + $parsed = ""; # Parsed text that will be returned. + + # + # Loop through every tag until we find the closing tag of the parent + # or loop until reaching the end of text if no parent tag specified. + # + do { + # + # Split the text using the first $tag_match pattern found. + # Text before pattern will be first in the array, text after + # pattern will be at the end, and between will be any catches made + # by the pattern. + # + $parts = preg_split($block_tag_re, $text, 2, + PREG_SPLIT_DELIM_CAPTURE); + + # If in Markdown span mode, add a empty-string span-level hash + # after each newline to prevent triggering any block element. + if ($span) { + $void = $this->hashPart("", ':'); + $newline = "$void\n"; + $parts[0] = $void . str_replace("\n", $newline, $parts[0]) . $void; + } + + $parsed .= $parts[0]; # Text before current tag. + + # If end of $text has been reached. Stop loop. + if (count($parts) < 3) { + $text = ""; + break; + } + + $tag = $parts[1]; # Tag to handle. + $text = $parts[2]; # Remaining text after current tag. + $tag_re = preg_quote($tag); # For use in a regular expression. + + # + # Check for: Code span marker + # + if ($tag{0} == "`") { + # Find corresponding end marker. + $tag_re = preg_quote($tag); + if (preg_match('{^(?>.+?|\n(?!\n))*?(?.*\n)+?'.$tag_re.' *\n}', $text, + $matches)) + { + # End marker found: pass text unchanged until marker. + $parsed .= $tag . $matches[0]; + $text = substr($text, strlen($matches[0])); + } + else { + # No end marker: just skip it. + $parsed .= $tag; + } + } + } + # + # Check for: Opening Block level tag or + # Opening Context Block tag (like ins and del) + # used as a block tag (tag is alone on it's line). + # + else if (preg_match('{^<(?:'.$this->block_tags_re.')\b}', $tag) || + ( preg_match('{^<(?:'.$this->context_block_tags_re.')\b}', $tag) && + preg_match($newline_before_re, $parsed) && + preg_match($newline_after_re, $text) ) + ) + { + # Need to parse tag and following text using the HTML parser. + list($block_text, $text) = + $this->_hashHTMLBlocks_inHTML($tag . $text, "hashBlock", true); + + # Make sure it stays outside of any paragraph by adding newlines. + $parsed .= "\n\n$block_text\n\n"; + } + # + # Check for: Clean tag (like script, math) + # HTML Comments, processing instructions. + # + else if (preg_match('{^<(?:'.$this->clean_tags_re.')\b}', $tag) || + $tag{1} == '!' || $tag{1} == '?') + { + # Need to parse tag and following text using the HTML parser. + # (don't check for markdown attribute) + list($block_text, $text) = + $this->_hashHTMLBlocks_inHTML($tag . $text, "hashClean", false); + + $parsed .= $block_text; + } + # + # Check for: Tag with same name as enclosing tag. + # + else if ($enclosing_tag_re !== '' && + # Same name as enclosing tag. + preg_match('{^= 0); + + return array($parsed, $text); + } + function _hashHTMLBlocks_inHTML($text, $hash_method, $md_attr) { + # + # Parse HTML, calling _HashHTMLBlocks_InMarkdown for block tags. + # + # * Calls $hash_method to convert any blocks. + # * Stops when the first opening tag closes. + # * $md_attr indicate if the use of the `markdown="1"` attribute is allowed. + # (it is not inside clean tags) + # + # Returns an array of that form: ( processed text , remaining text ) + # + if ($text === '') return array('', ''); + + # Regex to match `markdown` attribute inside of a tag. + $markdown_attr_re = ' + { + \s* # Eat whitespace before the `markdown` attribute + markdown + \s*=\s* + (?> + (["\']) # $1: quote delimiter + (.*?) # $2: attribute value + \1 # matching delimiter + | + ([^\s>]*) # $3: unquoted attribute value + ) + () # $4: make $3 always defined (avoid warnings) + }xs'; + + # Regex to match any tag. + $tag_re = '{ + ( # $2: Capture hole tag. + + ".*?" | # Double quotes (can contain `>`) + \'.*?\' | # Single quotes (can contain `>`) + .+? # Anything but quotes and `>`. + )*? + )? + > # End of tag. + | + # HTML Comment + | + <\?.*?\?> | <%.*?%> # Processing instruction + | + # CData Block + ) + }xs'; + + $original_text = $text; # Save original text in case of faliure. + + $depth = 0; # Current depth inside the tag tree. + $block_text = ""; # Temporary text holder for current text. + $parsed = ""; # Parsed text that will be returned. + + # + # Get the name of the starting tag. + # (This pattern makes $base_tag_name_re safe without quoting.) + # + if (preg_match('/^<([\w:$]*)\b/', $text, $matches)) + $base_tag_name_re = $matches[1]; + + # + # Loop through every tag until we find the corresponding closing tag. + # + do { + # + # Split the text using the first $tag_match pattern found. + # Text before pattern will be first in the array, text after + # pattern will be at the end, and between will be any catches made + # by the pattern. + # + $parts = preg_split($tag_re, $text, 2, PREG_SPLIT_DELIM_CAPTURE); + + if (count($parts) < 3) { + # + # End of $text reached with unbalenced tag(s). + # In that case, we return original text unchanged and pass the + # first character as filtered to prevent an infinite loop in the + # parent function. + # + return array($original_text{0}, substr($original_text, 1)); + } + + $block_text .= $parts[0]; # Text before current tag. + $tag = $parts[1]; # Tag to handle. + $text = $parts[2]; # Remaining text after current tag. + + # + # Check for: Auto-close tag (like
            ) + # Comments and Processing Instructions. + # + if (preg_match('{^auto_close_tags_re.')\b}', $tag) || + $tag{1} == '!' || $tag{1} == '?') + { + # Just add the tag to the block as if it was text. + $block_text .= $tag; + } + else { + # + # Increase/decrease nested tag count. Only do so if + # the tag's name match base tag's. + # + if (preg_match('{^mode = $attr_m[2] . $attr_m[3]; + $span_mode = $this->mode == 'span' || $this->mode != 'block' && + preg_match('{^<(?:'.$this->contain_span_tags_re.')\b}', $tag); + + # Calculate indent before tag. + if (preg_match('/(?:^|\n)( *?)(?! ).*?$/', $block_text, $matches)) { + $strlen = $this->utf8_strlen; + $indent = $strlen($matches[1], 'UTF-8'); + } else { + $indent = 0; + } + + # End preceding block with this tag. + $block_text .= $tag; + $parsed .= $this->$hash_method($block_text); + + # Get enclosing tag name for the ParseMarkdown function. + # (This pattern makes $tag_name_re safe without quoting.) + preg_match('/^<([\w:$]*)\b/', $tag, $matches); + $tag_name_re = $matches[1]; + + # Parse the content using the HTML-in-Markdown parser. + list ($block_text, $text) + = $this->_hashHTMLBlocks_inMarkdown($text, $indent, + $tag_name_re, $span_mode); + + # Outdent markdown text. + if ($indent > 0) { + $block_text = preg_replace("/^[ ]{1,$indent}/m", "", + $block_text); + } + + # Append tag content to parsed text. + if (!$span_mode) $parsed .= "\n\n$block_text\n\n"; + else $parsed .= "$block_text"; + + # Start over a new block. + $block_text = ""; + } + else $block_text .= $tag; + } + + } while ($depth > 0); + + # + # Hash last block text that wasn't processed inside the loop. + # + $parsed .= $this->$hash_method($block_text); + + return array($parsed, $text); + } + + + function hashClean($text) { + # + # Called whenever a tag must be hashed when a function insert a "clean" tag + # in $text, it pass through this function and is automaticaly escaped, + # blocking invalid nested overlap. + # + return $this->hashPart($text, 'C'); + } + + + function doHeaders($text) { + # + # Redefined to add id attribute support. + # + # Setext-style headers: + # Header 1 {#header1} + # ======== + # + # Header 2 {#header2} + # -------- + # + $text = preg_replace_callback( + '{ + (^.+?) # $1: Header text + (?:[ ]+\{\#([-_:a-zA-Z0-9]+)\})? # $2: Id attribute + [ ]*\n(=+|-+)[ ]*\n+ # $3: Header footer + }mx', + array(&$this, '_doHeaders_callback_setext'), $text); + + # atx-style headers: + # # Header 1 {#header1} + # ## Header 2 {#header2} + # ## Header 2 with closing hashes ## {#header3} + # ... + # ###### Header 6 {#header2} + # + $text = preg_replace_callback('{ + ^(\#{1,6}) # $1 = string of #\'s + [ ]* + (.+?) # $2 = Header text + [ ]* + \#* # optional closing #\'s (not counted) + (?:[ ]+\{\#([-_:a-zA-Z0-9]+)\})? # id attribute + [ ]* + \n+ + }xm', + array(&$this, '_doHeaders_callback_atx'), $text); + + return $text; + } + function _doHeaders_attr($attr) { + if (empty($attr)) return ""; + return " id=\"$attr\""; + } + function _doHeaders_callback_setext($matches) { + if ($matches[3] == '-' && preg_match('{^- }', $matches[1])) + return $matches[0]; + $level = $matches[3]{0} == '=' ? 1 : 2; + $attr = $this->_doHeaders_attr($id =& $matches[2]); + $block = "".$this->runSpanGamut($matches[1]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + function _doHeaders_callback_atx($matches) { + $level = strlen($matches[1]); + $attr = $this->_doHeaders_attr($id =& $matches[3]); + $block = "".$this->runSpanGamut($matches[2]).""; + return "\n" . $this->hashBlock($block) . "\n\n"; + } + + + function doTables($text) { + # + # Form HTML tables. + # + $less_than_tab = $this->tab_width - 1; + # + # Find tables with leading pipe. + # + # | Header 1 | Header 2 + # | -------- | -------- + # | Cell 1 | Cell 2 + # | Cell 3 | Cell 4 + # + $text = preg_replace_callback(' + { + ^ # Start of a line + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + [|] # Optional leading pipe (present) + (.+) \n # $1: Header row (at least one pipe) + + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + [|] ([ ]*[-:]+[-| :]*) \n # $2: Header underline + + ( # $3: Cells + (?> + [ ]* # Allowed whitespace. + [|] .* \n # Row content. + )* + ) + (?=\n|\Z) # Stop at final double newline. + }xm', + array(&$this, '_doTable_leadingPipe_callback'), $text); + + # + # Find tables without leading pipe. + # + # Header 1 | Header 2 + # -------- | -------- + # Cell 1 | Cell 2 + # Cell 3 | Cell 4 + # + $text = preg_replace_callback(' + { + ^ # Start of a line + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + (\S.*[|].*) \n # $1: Header row (at least one pipe) + + [ ]{0,'.$less_than_tab.'} # Allowed whitespace. + ([-:]+[ ]*[|][-| :]*) \n # $2: Header underline + + ( # $3: Cells + (?> + .* [|] .* \n # Row content + )* + ) + (?=\n|\Z) # Stop at final double newline. + }xm', + array(&$this, '_DoTable_callback'), $text); + + return $text; + } + function _doTable_leadingPipe_callback($matches) { + $head = $matches[1]; + $underline = $matches[2]; + $content = $matches[3]; + + # Remove leading pipe for each row. + $content = preg_replace('/^ *[|]/m', '', $content); + + return $this->_doTable_callback(array($matches[0], $head, $underline, $content)); + } + function _doTable_callback($matches) { + $head = $matches[1]; + $underline = $matches[2]; + $content = $matches[3]; + + # Remove any tailing pipes for each line. + $head = preg_replace('/[|] *$/m', '', $head); + $underline = preg_replace('/[|] *$/m', '', $underline); + $content = preg_replace('/[|] *$/m', '', $content); + + # Reading alignement from header underline. + $separators = preg_split('/ *[|] */', $underline); + foreach ($separators as $n => $s) { + if (preg_match('/^ *-+: *$/', $s)) $attr[$n] = ' align="right"'; + else if (preg_match('/^ *:-+: *$/', $s))$attr[$n] = ' align="center"'; + else if (preg_match('/^ *:-+ *$/', $s)) $attr[$n] = ' align="left"'; + else $attr[$n] = ''; + } + + # Parsing span elements, including code spans, character escapes, + # and inline HTML tags, so that pipes inside those gets ignored. + $head = $this->parseSpan($head); + $headers = preg_split('/ *[|] */', $head); + $col_count = count($headers); + + # Write column headers. + $text = "\n"; + $text .= "\n"; + $text .= "\n"; + foreach ($headers as $n => $header) + $text .= " ".$this->runSpanGamut(trim($header))."\n"; + $text .= "\n"; + $text .= "\n"; + + # Split content by row. + $rows = explode("\n", trim($content, "\n")); + + $text .= "\n"; + foreach ($rows as $row) { + # Parsing span elements, including code spans, character escapes, + # and inline HTML tags, so that pipes inside those gets ignored. + $row = $this->parseSpan($row); + + # Split row by cell. + $row_cells = preg_split('/ *[|] */', $row, $col_count); + $row_cells = array_pad($row_cells, $col_count, ''); + + $text .= "\n"; + foreach ($row_cells as $n => $cell) + $text .= " ".$this->runSpanGamut(trim($cell))."\n"; + $text .= "\n"; + } + $text .= "\n"; + $text .= "
            "; + + return $this->hashBlock($text) . "\n"; + } + + + function doDefLists($text) { + # + # Form HTML definition lists. + # + $less_than_tab = $this->tab_width - 1; + + # Re-usable pattern to match any entire dl list: + $whole_list_re = '(?> + ( # $1 = whole list + ( # $2 + [ ]{0,'.$less_than_tab.'} + ((?>.*\S.*\n)+) # $3 = defined term + \n? + [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition + ) + (?s:.+?) + ( # $4 + \z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another term + [ ]{0,'.$less_than_tab.'} + (?: \S.*\n )+? # defined term + \n? + [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition + ) + (?! # Negative lookahead for another definition + [ ]{0,'.$less_than_tab.'}:[ ]+ # colon starting definition + ) + ) + ) + )'; // mx + + $text = preg_replace_callback('{ + (?>\A\n?|(?<=\n\n)) + '.$whole_list_re.' + }mx', + array(&$this, '_doDefLists_callback'), $text); + + return $text; + } + function _doDefLists_callback($matches) { + # Re-usable patterns to match list item bullets and number markers: + $list = $matches[1]; + + # Turn double returns into triple returns, so that we can make a + # paragraph for the last item in a list, if necessary: + $result = trim($this->processDefListItems($list)); + $result = "
            \n" . $result . "\n
            "; + return $this->hashBlock($result) . "\n\n"; + } + + + function processDefListItems($list_str) { + # + # Process the contents of a single definition list, splitting it + # into individual term and definition list items. + # + $less_than_tab = $this->tab_width - 1; + + # trim trailing blank lines: + $list_str = preg_replace("/\n{2,}\\z/", "\n", $list_str); + + # Process definition terms. + $list_str = preg_replace_callback('{ + (?>\A\n?|\n\n+) # leading line + ( # definition terms = $1 + [ ]{0,'.$less_than_tab.'} # leading whitespace + (?![:][ ]|[ ]) # negative lookahead for a definition + # mark (colon) or more whitespace. + (?> \S.* \n)+? # actual term (not whitespace). + ) + (?=\n?[ ]{0,3}:[ ]) # lookahead for following line feed + # with a definition mark. + }xm', + array(&$this, '_processDefListItems_callback_dt'), $list_str); + + # Process actual definitions. + $list_str = preg_replace_callback('{ + \n(\n+)? # leading line = $1 + ( # marker space = $2 + [ ]{0,'.$less_than_tab.'} # whitespace before colon + [:][ ]+ # definition mark (colon) + ) + ((?s:.+?)) # definition text = $3 + (?= \n+ # stop at next definition mark, + (?: # next term or end of text + [ ]{0,'.$less_than_tab.'} [:][ ] | +
            | \z + ) + ) + }xm', + array(&$this, '_processDefListItems_callback_dd'), $list_str); + + return $list_str; + } + function _processDefListItems_callback_dt($matches) { + $terms = explode("\n", trim($matches[1])); + $text = ''; + foreach ($terms as $term) { + $term = $this->runSpanGamut(trim($term)); + $text .= "\n
            " . $term . "
            "; + } + return $text . "\n"; + } + function _processDefListItems_callback_dd($matches) { + $leading_line = $matches[1]; + $marker_space = $matches[2]; + $def = $matches[3]; + + if ($leading_line || preg_match('/\n{2,}/', $def)) { + # Replace marker with the appropriate whitespace indentation + $def = str_repeat(' ', strlen($marker_space)) . $def; + $def = $this->runBlockGamut($this->outdent($def . "\n\n")); + $def = "\n". $def ."\n"; + } + else { + $def = rtrim($def); + $def = $this->runSpanGamut($this->outdent($def)); + } + + return "\n
            " . $def . "
            \n"; + } + + + function doFencedCodeBlocks($text) { + # + # Adding the fenced code block syntax to regular Markdown: + # + # ~~~ + # Code block + # ~~~ + # + $less_than_tab = $this->tab_width; + + $text = preg_replace_callback('{ + (?:\n|\A) + # 1: Opening marker + ( + ~{3,} # Marker: three tilde or more. + ) + [ ]* \n # Whitespace and newline following marker. + + # 2: Content + ( + (?> + (?!\1 [ ]* \n) # Not a closing marker. + .*\n+ + )+ + ) + + # Closing marker. + \1 [ ]* \n + }xm', + array(&$this, '_doFencedCodeBlocks_callback'), $text); + + return $text; + } + function _doFencedCodeBlocks_callback($matches) { + $codeblock = $matches[2]; + $codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES); + $codeblock = preg_replace_callback('/^\n+/', + array(&$this, '_doFencedCodeBlocks_newlines'), $codeblock); + $codeblock = "
            $codeblock
            "; + return "\n\n".$this->hashBlock($codeblock)."\n\n"; + } + function _doFencedCodeBlocks_newlines($matches) { + return str_repeat("empty_element_suffix", + strlen($matches[0])); + } + + + # + # Redefining emphasis markers so that emphasis by underscore does not + # work in the middle of a word. + # + var $em_relist = array( + '' => '(?:(? '(?<=\S)(? '(?<=\S)(? '(?:(? '(?<=\S)(? '(?<=\S)(? '(?:(? '(?<=\S)(? '(?<=\S)(? tags + # + # Strip leading and trailing lines: + $text = preg_replace('/\A\n+|\n+\z/', '', $text); + + $grafs = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY); + + # + # Wrap

            tags and unhashify HTML blocks + # + foreach ($grafs as $key => $value) { + $value = trim($this->runSpanGamut($value)); + + # Check if this should be enclosed in a paragraph. + # Clean tag hashes & block tag hashes are left alone. + $is_p = !preg_match('/^B\x1A[0-9]+B|^C\x1A[0-9]+C$/', $value); + + if ($is_p) { + $value = "

            $value

            "; + } + $grafs[$key] = $value; + } + + # Join grafs in one text, then unhash HTML tags. + $text = implode("\n\n", $grafs); + + # Finish by removing any tag hashes still present in $text. + $text = $this->unhash($text); + + return $text; + } + + + ### Footnotes + + function stripFootnotes($text) { + # + # Strips link definitions from text, stores the URLs and titles in + # hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: [^id]: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\[\^(.+?)\][ ]?: # note_id = $1 + [ ]* + \n? # maybe *one* newline + ( # text = $2 (no blank lines allowed) + (?: + .+ # actual text + | + \n # newlines but + (?!\[\^.+?\]:\s)# negative lookahead for footnote marker. + (?!\n+[ ]{0,3}\S)# ensure line is not blank and followed + # by non-indented content + )* + ) + }xm', + array(&$this, '_stripFootnotes_callback'), + $text); + return $text; + } + function _stripFootnotes_callback($matches) { + $note_id = $this->fn_id_prefix . $matches[1]; + $this->footnotes[$note_id] = $this->outdent($matches[2]); + return ''; # String that will replace the block + } + + + function doFootnotes($text) { + # + # Replace footnote references in $text [^id] with a special text-token + # which will be replaced by the actual footnote marker in appendFootnotes. + # + if (!$this->in_anchor) { + $text = preg_replace('{\[\^(.+?)\]}', "F\x1Afn:\\1\x1A:", $text); + } + return $text; + } + + + function appendFootnotes($text) { + # + # Append footnote list to text. + # + $text = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', + array(&$this, '_appendFootnotes_callback'), $text); + + if (!empty($this->footnotes_ordered)) { + $text .= "\n\n"; + $text .= "
            \n"; + $text .= "fn_backlink_class != "") { + $class = $this->fn_backlink_class; + $class = $this->encodeAttribute($class); + $attr .= " class=\"$class\""; + } + if ($this->fn_backlink_title != "") { + $title = $this->fn_backlink_title; + $title = $this->encodeAttribute($title); + $attr .= " title=\"$title\""; + } + $num = 0; + + while (!empty($this->footnotes_ordered)) { + $footnote = reset($this->footnotes_ordered); + $note_id = key($this->footnotes_ordered); + unset($this->footnotes_ordered[$note_id]); + + $footnote .= "\n"; # Need to append newline before parsing. + $footnote = $this->runBlockGamut("$footnote\n"); + $footnote = preg_replace_callback('{F\x1Afn:(.*?)\x1A:}', + array(&$this, '_appendFootnotes_callback'), $footnote); + + $attr = str_replace("%%", ++$num, $attr); + $note_id = $this->encodeAttribute($note_id); + + # Add backlink to last paragraph; create new paragraph if needed. + $backlink = ""; + if (preg_match('{

            $}', $footnote)) { + $footnote = substr($footnote, 0, -4) . " $backlink

            "; + } else { + $footnote .= "\n\n

            $backlink

            "; + } + + $text .= "
          • \n"; + $text .= $footnote . "\n"; + $text .= "
          • \n\n"; + } + + $text .= "\n"; + $text .= "
            "; + } + return $text; + } + function _appendFootnotes_callback($matches) { + $node_id = $this->fn_id_prefix . $matches[1]; + + # Create footnote marker only if it has a corresponding footnote *and* + # the footnote hasn't been used by another marker. + if (isset($this->footnotes[$node_id])) { + # Transfert footnote content to the ordered list. + $this->footnotes_ordered[$node_id] = $this->footnotes[$node_id]; + unset($this->footnotes[$node_id]); + + $num = $this->footnote_counter++; + $attr = " rel=\"footnote\""; + if ($this->fn_link_class != "") { + $class = $this->fn_link_class; + $class = $this->encodeAttribute($class); + $attr .= " class=\"$class\""; + } + if ($this->fn_link_title != "") { + $title = $this->fn_link_title; + $title = $this->encodeAttribute($title); + $attr .= " title=\"$title\""; + } + + $attr = str_replace("%%", $num, $attr); + $node_id = $this->encodeAttribute($node_id); + + return + "". + "$num". + ""; + } + + return "[^".$matches[1]."]"; + } + + + ### Abbreviations ### + + function stripAbbreviations($text) { + # + # Strips abbreviations from text, stores titles in hash references. + # + $less_than_tab = $this->tab_width - 1; + + # Link defs are in the form: [id]*: url "optional title" + $text = preg_replace_callback('{ + ^[ ]{0,'.$less_than_tab.'}\*\[(.+?)\][ ]?: # abbr_id = $1 + (.*) # text = $2 (no blank lines allowed) + }xm', + array(&$this, '_stripAbbreviations_callback'), + $text); + return $text; + } + function _stripAbbreviations_callback($matches) { + $abbr_word = $matches[1]; + $abbr_desc = $matches[2]; + if ($this->abbr_word_re) + $this->abbr_word_re .= '|'; + $this->abbr_word_re .= preg_quote($abbr_word); + $this->abbr_desciptions[$abbr_word] = trim($abbr_desc); + return ''; # String that will replace the block + } + + + function doAbbreviations($text) { + # + # Find defined abbreviations in text and wrap them in elements. + # + if ($this->abbr_word_re) { + // cannot use the /x modifier because abbr_word_re may + // contain significant spaces: + $text = preg_replace_callback('{'. + '(?abbr_word_re.')'. + '(?![\w\x1A])'. + '}', + array(&$this, '_doAbbreviations_callback'), $text); + } + return $text; + } + function _doAbbreviations_callback($matches) { + $abbr = $matches[0]; + if (isset($this->abbr_desciptions[$abbr])) { + $desc = $this->abbr_desciptions[$abbr]; + if (empty($desc)) { + return $this->hashPart("$abbr"); + } else { + $desc = $this->encodeAttribute($desc); + return $this->hashPart("$abbr"); + } + } else { + return $matches[0]; + } + } + +} + + +/* + +PHP Markdown Extra +================== + +Description +----------- + +This is a PHP port of the original Markdown formatter written in Perl +by John Gruber. This special "Extra" version of PHP Markdown features +further enhancements to the syntax for making additional constructs +such as tables and definition list. + +Markdown is a text-to-HTML filter; it translates an easy-to-read / +easy-to-write structured text format into HTML. Markdown's text format +is most similar to that of plain text email, and supports features such +as headers, *emphasis*, code blocks, blockquotes, and links. + +Markdown's syntax is designed not as a generic markup language, but +specifically to serve as a front-end to (X)HTML. You can use span-level +HTML tags anywhere in a Markdown document, and you can use block level +HTML tags (like
            and as well). + +For more information about Markdown's syntax, see: + + + + +Bugs +---- + +To file bug reports please send email to: + + + +Please include with your report: (1) the example input; (2) the output you +expected; (3) the output Markdown actually produced. + + +Version History +--------------- + +See the readme file for detailed release notes for this version. + + +Copyright and License +--------------------- + +PHP Markdown & Extra +Copyright (c) 2004-2008 Michel Fortin + +All rights reserved. + +Based on Markdown +Copyright (c) 2003-2006 John Gruber + +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +* Neither the name "Markdown" nor the names of its contributors may + be used to endorse or promote products derived from this software + without specific prior written permission. + +This software is provided by the copyright holders and contributors "as +is" and any express or implied warranties, including, but not limited +to, the implied warranties of merchantability and fitness for a +particular purpose are disclaimed. In no event shall the copyright owner +or contributors be liable for any direct, indirect, incidental, special, +exemplary, or consequential damages (including, but not limited to, +procurement of substitute goods or services; loss of use, data, or +profits; or business interruption) however caused and on any theory of +liability, whether in contract, strict liability, or tort (including +negligence or otherwise) arising in any way out of the use of this +software, even if advised of the possibility of such damage. + +*/ +?> \ No newline at end of file diff --git a/includes/kohana/modules/userguide/views/userguide/api/class.php b/includes/kohana/modules/userguide/views/userguide/api/class.php new file mode 100644 index 00000000..78bf73de --- /dev/null +++ b/includes/kohana/modules/userguide/views/userguide/api/class.php @@ -0,0 +1,93 @@ +

            + modifiers, $doc->class->name ?> + class; ?> + getParentClass()): ?> +
            uri(array('class' => $parent->name)), $parent->name) ?> + +

            + +

            +
            +
            +

            +
              + constants): ?> + constants as $name => $value): ?> +
            • + + +
            • + +
            +
            +
            +

            + +
            +
            +

            + +
            +
            + +description ?> + +tags) echo View::factory('userguide/api/tags')->set('tags', $doc->tags) ?> + +

            +class->getFilename()): ?> +Class declared in on line class->getStartLine() ?>. + +Class is not declared in a file, it is probably an internal class->name).'.php', 'PHP class') ?>. + +

            + +constants): ?> +
            +

            +
            +constants as $name => $value): ?> +
            +
            + +
            +
            + + +properties()): ?> +

            +
            +
            + +
            modifiers ?> type ?> $property->name ?>
            +
            description ?>
            +
            value ?>
            + +
            +
            + + +methods()): ?> +

            +
            + +set('doc', $method)->set('route', $route) ?> + +
            + diff --git a/includes/kohana/modules/userguide/views/userguide/api/menu.php b/includes/kohana/modules/userguide/views/userguide/api/menu.php new file mode 100644 index 00000000..7d9bbe83 --- /dev/null +++ b/includes/kohana/modules/userguide/views/userguide/api/menu.php @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/includes/kohana/modules/userguide/views/userguide/api/method.php b/includes/kohana/modules/userguide/views/userguide/api/method.php new file mode 100644 index 00000000..6a2eb550 --- /dev/null +++ b/includes/kohana/modules/userguide/views/userguide/api/method.php @@ -0,0 +1,52 @@ +
            + +method->getDeclaringClass(); ?> +

            + modifiers, $doc->method->name ?>( params ? $doc->params_short() : '' ?>) +
            uri(array('class' => $declares->name)), $declares->name) ?> +

            + +
            +description ?> +
            + +tags) echo View::factory('userguide/api/tags')->set('tags', $doc->tags) ?> + + +params): ?> +
            +
            + + + + + + +params as $param): ?> + + + + + + + +
            name ?>byref?'byref ':''.$param->type?$param->type:'unknown' ?>description) ?>default ?>
            + + +return): ?> +
            +
              +return as $set): list($type, $text) = $set; ?> +
            • + +
            + + +source): ?> +
            +
            +
            source) ?>
            +
            + + +
            diff --git a/includes/kohana/modules/userguide/views/userguide/api/tags.php b/includes/kohana/modules/userguide/views/userguide/api/tags.php new file mode 100644 index 00000000..32e2405d --- /dev/null +++ b/includes/kohana/modules/userguide/views/userguide/api/tags.php @@ -0,0 +1,8 @@ +
            + $set): ?> +
            + +
            + + +
            \ No newline at end of file diff --git a/includes/kohana/modules/userguide/views/userguide/api/toc.php b/includes/kohana/modules/userguide/views/userguide/api/toc.php new file mode 100644 index 00000000..627c3f5e --- /dev/null +++ b/includes/kohana/modules/userguide/views/userguide/api/toc.php @@ -0,0 +1,16 @@ +

            + +
            + + $methods): $link = $route->uri(array('class' => $class)) ?> +
            +

            +
              + +
            • + +
            +
            + + +
            diff --git a/includes/kohana/modules/userguide/views/userguide/error.php b/includes/kohana/modules/userguide/views/userguide/error.php new file mode 100644 index 00000000..1f7b97b7 --- /dev/null +++ b/includes/kohana/modules/userguide/views/userguide/error.php @@ -0,0 +1,3 @@ +

            Kodoc -

            + +

            \ No newline at end of file diff --git a/includes/kohana/modules/userguide/views/userguide/examples/error.php b/includes/kohana/modules/userguide/views/userguide/examples/error.php new file mode 100644 index 00000000..81548cee --- /dev/null +++ b/includes/kohana/modules/userguide/views/userguide/examples/error.php @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/includes/kohana/modules/userguide/views/userguide/examples/hello_world_error.php b/includes/kohana/modules/userguide/views/userguide/examples/hello_world_error.php new file mode 100644 index 00000000..d5dcf442 --- /dev/null +++ b/includes/kohana/modules/userguide/views/userguide/examples/hello_world_error.php @@ -0,0 +1,696 @@ + + +
            +

            Kohana_View_Exception [ 0 ]: The requested view site could not be found

            +
            +

            SYSPATH/classes/kohana/view.php [ 215 ]

            +
            210 	 */
            +
            +211 	public function set_filename($file)
            +212 	{
            +213 		if (($path = Kohana::find_file('views', $file)) === FALSE)
            +214 		{
            +215 			throw new Kohana_View_Exception('The requested view :file could not be found', array(
            +216 				':file' => $file,
            +
            +217 			));
            +218 		}
            +219 
            +220 		// Store the file path locally
            +
              +
            1. +

              + + + SYSPATH/classes/kohana/view.php [ 115 ] + + » + Kohana_View->set_filename(arguments) +

              + + + +
            2. + +
            3. +

              + + SYSPATH/classes/kohana/view.php [ 26 ] + + » + Kohana_View->__construct(arguments) +

              + + + +
            4. +
            5. +

              + + + SYSPATH/classes/kohana/controller/template.php [ 32 ] + + » + Kohana_View::factory(arguments) +

              + + + +
            6. +
            7. +

              + + {PHP internal call} + + » + Kohana_Controller_Template->before() +

              + +
            8. +
            9. +

              + + SYSPATH/classes/kohana/request.php [ 840 ] + + » + ReflectionMethod->invoke(arguments) +

              + + + +
            10. +
            11. +

              + + APPPATH/bootstrap.php [ 76 ] + + + » + Kohana_Request->execute() +

              + +
            12. +
            13. +

              + + DOCROOT/index.php [ 106 ] + + » + require(arguments) +

              + + + +
            14. +
            +
            +

            Environment

            + +
            diff --git a/includes/kohana/modules/userguide/views/userguide/template.php b/includes/kohana/modules/userguide/views/userguide/template.php new file mode 100644 index 00000000..49d6270c --- /dev/null +++ b/includes/kohana/modules/userguide/views/userguide/template.php @@ -0,0 +1,89 @@ + + + + + +<?php echo $title ?> | Kohana <?php echo __('User Guide'); ?> + + $media) echo HTML::style($style, array('media' => $media), TRUE), "\n" ?> + + + + + + +
            +
            +
            + +
            + +
            + 'get')) ?> + + +
            +
            +
            + +
            +
            + + + +
            + + + Documentation comments powered by Disqus + +
            + + +
            + + + + + + + + diff --git a/includes/kohana/system/base.php b/includes/kohana/system/base.php new file mode 100644 index 00000000..1e637979 --- /dev/null +++ b/includes/kohana/system/base.php @@ -0,0 +1,43 @@ + $username)); + * + * [!!] The target language is defined by [I18n::$lang]. + * + * @uses I18n::get + * @param string text to translate + * @param array values to replace in the translated text + * @param string source language + * @return string + */ +function __($string, array $values = NULL, $lang = 'en-us') +{ + if ($lang !== I18n::$lang) + { + // The message and target languages are different + // Get the translation for this message + $string = I18n::get($string); + } + + return empty($values) ? $string : strtr($string, $values); +} diff --git a/includes/kohana/system/classes/arr.php b/includes/kohana/system/classes/arr.php new file mode 100644 index 00000000..a2798312 --- /dev/null +++ b/includes/kohana/system/classes/arr.php @@ -0,0 +1,3 @@ + 'john.doe')); + * + * // Returns FALSE + * Arr::is_assoc('foo', 'bar'); + * + * @param array array to check + * @return boolean + */ + public static function is_assoc(array $array) + { + // Keys of the array + $keys = array_keys($array); + + // If the array keys of the keys match the keys, then the array must + // not be associative (e.g. the keys array looked like {0:0, 1:1...}). + return array_keys($keys) !== $keys; + } + + /** + * Gets a value from an array using a dot separated path. + * + * // Get the value of $array['foo']['bar'] + * $value = Arr::path($array, 'foo.bar'); + * + * Using a wildcard "*" will search intermediate arrays and return an array. + * + * // Get the values of "color" in theme + * $colors = Arr::path($array, 'theme.*.color'); + * + * @param array array to search + * @param string key path, delimiter separated + * @param mixed default value if the path is not set + * @param string key path delimiter + * @return mixed + */ + public static function path($array, $path, $default = NULL, $delimiter = NULL) + { + if (array_key_exists($path, $array)) + { + // No need to do extra processing + return $array[$path]; + } + + if ($delimiter === NULL) + { + // Use the default delimiter + $delimiter = Arr::$delimiter; + } + + // Remove outer delimiters, wildcards, or spaces + $path = trim($path, "{$delimiter}* "); + + // Split the keys by delimiter + $keys = explode($delimiter, $path); + + do + { + $key = array_shift($keys); + + if (ctype_digit($key)) + { + // Make the key an integer + $key = (int) $key; + } + + if (isset($array[$key])) + { + if ($keys) + { + if (is_array($array[$key])) + { + // Dig down into the next part of the path + $array = $array[$key]; + } + else + { + // Unable to dig deeper + break; + } + } + else + { + // Found the path requested + return $array[$key]; + } + } + elseif ($key === '*') + { + // Handle wildcards + + $values = array(); + foreach ($array as $arr) + { + if ($value = Arr::path($arr, implode('.', $keys))) + { + $values[] = $value; + } + } + + if ($values) + { + // Found the values requested + return $values; + } + else + { + // Unable to dig deeper + break; + } + } + else + { + // Unable to dig deeper + break; + } + } + while ($keys); + + // Unable to find the value requested + return $default; + } + + /** + * Fill an array with a range of numbers. + * + * // Fill an array with values 5, 10, 15, 20 + * $values = Arr::range(5, 20); + * + * @param integer stepping + * @param integer ending number + * @return array + */ + public static function range($step = 10, $max = 100) + { + if ($step < 1) + return array(); + + $array = array(); + for ($i = $step; $i <= $max; $i += $step) + { + $array[$i] = $i; + } + + return $array; + } + + /** + * Retrieve a single key from an array. If the key does not exist in the + * array, the default value will be returned instead. + * + * // Get the value "username" from $_POST, if it exists + * $username = Arr::get($_POST, 'username'); + * + * // Get the value "sorting" from $_GET, if it exists + * $sorting = Arr::get($_GET, 'sorting'); + * + * @param array array to extract from + * @param string key name + * @param mixed default value + * @return mixed + */ + public static function get($array, $key, $default = NULL) + { + return isset($array[$key]) ? $array[$key] : $default; + } + + /** + * Retrieves multiple keys from an array. If the key does not exist in the + * array, the default value will be added instead. + * + * // Get the values "username", "password" from $_POST + * $auth = Arr::extract($_POST, array('username', 'password')); + * + * @param array array to extract keys from + * @param array list of key names + * @param mixed default value + * @return array + */ + public static function extract($array, array $keys, $default = NULL) + { + $found = array(); + foreach ($keys as $key) + { + $found[$key] = isset($array[$key]) ? $array[$key] : $default; + } + + return $found; + } + + /** + * Binary search algorithm. + * + * @deprecated Use [array_search](http://php.net/array_search) instead + * + * @param mixed the value to search for + * @param array an array of values to search in + * @param boolean sort the array now + * @return integer the index of the match + * @return FALSE no matching index found + */ + public static function binary_search($needle, $haystack, $sort = FALSE) + { + return array_search($needle, $haystack); + } + + /** + * Adds a value to the beginning of an associative array. + * + * // Add an empty value to the start of a select list + * Arr::unshift_assoc($array, 'none', 'Select a value'); + * + * @param array array to modify + * @param string array key name + * @param mixed array value + * @return array + */ + public static function unshift( array & $array, $key, $val) + { + $array = array_reverse($array, TRUE); + $array[$key] = $val; + $array = array_reverse($array, TRUE); + + return $array; + } + + /** + * Recursive version of [array_map](http://php.net/array_map), applies the + * same callback to all elements in an array, including sub-arrays. + * + * // Apply "strip_tags" to every element in the array + * $array = Arr::map('strip_tags', $array); + * + * [!!] Unlike `array_map`, this method requires a callback and will only map + * a single array. + * + * @param mixed callback applied to every element in the array + * @param array array to map + * @return array + */ + public static function map($callback, $array) + { + foreach ($array as $key => $val) + { + if (is_array($val)) + { + $array[$key] = Arr::map($callback, $val); + } + else + { + $array[$key] = call_user_func($callback, $val); + } + } + + return $array; + } + + /** + * Merges one or more arrays recursively and preserves all keys. + * Note that this does not work the same as [array_merge_recursive](http://php.net/array_merge_recursive)! + * + * $john = array('name' => 'john', 'children' => array('fred', 'paul', 'sally', 'jane')); + * $mary = array('name' => 'mary', 'children' => array('jane')); + * + * // John and Mary are married, merge them together + * $john = Arr::merge($john, $mary); + * + * // The output of $john will now be: + * array('name' => 'mary', 'children' => array('fred', 'paul', 'sally', 'jane')) + * + * @param array initial array + * @param array array to merge + * @param array ... + * @return array + */ + public static function merge(array $a1, array $a2) + { + $result = array(); + for ($i = 0, $total = func_num_args(); $i < $total; $i++) + { + // Get the next array + $arr = func_get_arg($i); + + // Is the array associative? + $assoc = Arr::is_assoc($arr); + + foreach ($arr as $key => $val) + { + if (isset($result[$key])) + { + if (is_array($val) && is_array($result[$key])) + { + if (Arr::is_assoc($val)) + { + // Associative arrays are merged recursively + $result[$key] = Arr::merge($result[$key], $val); + } + else + { + // Find the values that are not already present + $diff = array_diff($val, $result[$key]); + + // Indexed arrays are merged to prevent duplicates + $result[$key] = array_merge($result[$key], $diff); + } + } + else + { + if ($assoc) + { + // Associative values are replaced + $result[$key] = $val; + } + elseif ( ! in_array($val, $result, TRUE)) + { + // Indexed values are added only if they do not yet exist + $result[] = $val; + } + } + } + else + { + // New values are added + $result[$key] = $val; + } + } + } + + return $result; + } + + /** + * Overwrites an array with values from input arrays. + * Keys that do not exist in the first array will not be added! + * + * $a1 = array('name' => 'john', 'mood' => 'happy', 'food' => 'bacon'); + * $a2 = array('name' => 'jack', 'food' => 'tacos', 'drink' => 'beer'); + * + * // Overwrite the values of $a1 with $a2 + * $array = Arr::overwrite($a1, $a2); + * + * // The output of $array will now be: + * array('name' => 'jack', 'mood' => 'happy', 'food' => 'bacon') + * + * @param array master array + * @param array input arrays that will overwrite existing values + * @return array + */ + public static function overwrite($array1, $array2) + { + foreach (array_intersect_key($array2, $array1) as $key => $value) + { + $array1[$key] = $value; + } + + if (func_num_args() > 2) + { + foreach (array_slice(func_get_args(), 2) as $array2) + { + foreach (array_intersect_key($array2, $array1) as $key => $value) + { + $array1[$key] = $value; + } + } + } + + return $array1; + } + + /** + * Creates a callable function and parameter list from a string representation. + * Note that this function does not validate the callback string. + * + * // Get the callback function and parameters + * list($func, $params) = Arr::callback('Foo::bar(apple,orange)'); + * + * // Get the result of the callback + * $result = call_user_func_array($func, $params); + * + * @param string callback string + * @return array function, params + */ + public static function callback($str) + { + // Overloaded as parts are found + $command = $params = NULL; + + // command[param,param] + if (preg_match('/^([^\(]*+)\((.*)\)$/', $str, $match)) + { + // command + $command = $match[1]; + + if ($match[2] !== '') + { + // param,param + $params = preg_split('/(? array('one' => 'something'), 'two' => 'other'); + * + * // Flatten the array + * $array = Arr::flatten($array); + * + * // The array will now be + * array('one' => 'something', 'two' => 'other'); + * + * [!!] The keys of array values will be discarded. + * + * @param array array to flatten + * @return array + * @since 3.0.6 + */ + public static function flatten($array) + { + $flat = array(); + foreach ($array as $key => $value) + { + if (is_array($value)) + { + $flat += Arr::flatten($value); + } + else + { + $flat[$key] = $value; + } + } + return $flat; + } + +} // End arr diff --git a/includes/kohana/system/classes/kohana/cli.php b/includes/kohana/system/classes/kohana/cli.php new file mode 100644 index 00000000..140e43fe --- /dev/null +++ b/includes/kohana/system/classes/kohana/cli.php @@ -0,0 +1,75 @@ +attach($reader); // Try first + * $config->attach($reader, FALSE); // Try last + * + * @param object Kohana_Config_Reader instance + * @param boolean add the reader as the first used object + * @return $this + */ + public function attach(Kohana_Config_Reader $reader, $first = TRUE) + { + if ($first === TRUE) + { + // Place the log reader at the top of the stack + array_unshift($this->_readers, $reader); + } + else + { + // Place the reader at the bottom of the stack + $this->_readers[] = $reader; + } + + return $this; + } + + /** + * Detach a configuration reader. + * + * $config->detach($reader); + * + * @param object Kohana_Config_Reader instance + * @return $this + */ + public function detach(Kohana_Config_Reader $reader) + { + if (($key = array_search($reader, $this->_readers)) !== FALSE) + { + // Remove the writer + unset($this->_readers[$key]); + } + + return $this; + } + + /** + * Load a configuration group. Searches the readers in order until the + * group is found. If the group does not exist, an empty configuration + * array will be loaded using the first reader. + * + * $array = $config->load($name); + * + * @param string configuration group name + * @return object Kohana_Config_Reader + * @throws Kohana_Exception + */ + public function load($group) + { + foreach ($this->_readers as $reader) + { + if ($config = $reader->load($group)) + { + // Found a reader for this configuration group + return $config; + } + } + + // Reset the iterator + reset($this->_readers); + + if ( ! is_object($config = current($this->_readers))) + { + throw new Kohana_Exception('No configuration readers attached'); + } + + // Load the reader as an empty array + return $config->load($group, array()); + } + + /** + * Copy one configuration group to all of the other readers. + * + * $config->copy($name); + * + * @param string configuration group name + * @return $this + */ + public function copy($group) + { + // Load the configuration group + $config = $this->load($group); + + foreach ($this->_readers as $reader) + { + if ($config instanceof $reader) + { + // Do not copy the config to the same group + continue; + } + + // Load the configuration object + $object = $reader->load($group, array()); + + foreach ($config as $key => $value) + { + // Copy each value in the config + $object->offsetSet($key, $value); + } + } + + return $this; + } + +} // End Kohana_Config diff --git a/includes/kohana/system/classes/kohana/config/file.php b/includes/kohana/system/classes/kohana/config/file.php new file mode 100644 index 00000000..16d52c2e --- /dev/null +++ b/includes/kohana/system/classes/kohana/config/file.php @@ -0,0 +1,56 @@ +_directory = trim($directory, '/'); + + // Load the empty array + parent::__construct(); + } + + /** + * Load and merge all of the configuration files in this group. + * + * $config->load($name); + * + * @param string configuration group name + * @param array configuration array + * @return $this clone of the current object + * @uses Kohana::load + */ + public function load($group, array $config = NULL) + { + if ($files = Kohana::find_file($this->_directory, $group, NULL, TRUE)) + { + // Initialize the config array + $config = array(); + + foreach ($files as $file) + { + // Merge each file to the configuration array + $config = Arr::merge($config, Kohana::load($file)); + } + } + + return parent::load($group, $config); + } + +} // End Kohana_Config diff --git a/includes/kohana/system/classes/kohana/config/reader.php b/includes/kohana/system/classes/kohana/config/reader.php new file mode 100644 index 00000000..8edd8174 --- /dev/null +++ b/includes/kohana/system/classes/kohana/config/reader.php @@ -0,0 +1,113 @@ +getArrayCopy()); + } + + /** + * Loads a configuration group. + * + * $config->load($name, $array); + * + * This method must be extended by all readers. After the group has been + * loaded, call `parent::load($group, $config)` for final preparation. + * + * @param string configuration group name + * @param array configuration array + * @return $this a clone of this object + */ + public function load($group, array $config = NULL) + { + if ($config === NULL) + { + return FALSE; + } + + // Clone the current object + $object = clone $this; + + // Set the group name + $object->_configuration_group = $group; + + // Swap the array with the actual configuration + $object->exchangeArray($config); + + return $object; + } + + /** + * Return the raw array that is being used for this object. + * + * $array = $config->as_array(); + * + * @return array + */ + public function as_array() + { + return $this->getArrayCopy(); + } + + /** + * Get a variable from the configuration or return the default value. + * + * $value = $config->get($key); + * + * @param string array key + * @param mixed default value + * @return mixed + */ + public function get($key, $default = NULL) + { + return $this->offsetExists($key) ? $this->offsetGet($key) : $default; + } + + /** + * Sets a value in the configuration array. + * + * $config->set($key, $new_value); + * + * @param string array key + * @param mixed array value + * @return $this + */ + public function set($key, $value) + { + $this->offsetSet($key, $value); + + return $this; + } + +} // End Kohana_Config_Reader diff --git a/includes/kohana/system/classes/kohana/controller.php b/includes/kohana/system/classes/kohana/controller.php new file mode 100644 index 00000000..d70fda68 --- /dev/null +++ b/includes/kohana/system/classes/kohana/controller.php @@ -0,0 +1,66 @@ +before(); + * $controller->action_bar(); + * $controller->after(); + * + * The controller action should add the output it creates to + * `$this->request->response`, typically in the form of a [View], during the + * "action" part of execution. + * + * @package Kohana + * @category Controller + * @author Kohana Team + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +abstract class Kohana_Controller { + + /** + * @var object Request that created the controller + */ + public $request; + + /** + * Creates a new controller instance. Each controller must be constructed + * with the request object that created it. + * + * @param object Request that created the controller + * @return void + */ + public function __construct(Kohana_Request $request) + { + // Assign the request to the controller + $this->request = $request; + } + + /** + * Automatically executed before the controller action. Can be used to set + * class properties, do authorization checks, and execute other custom code. + * + * @return void + */ + public function before() + { + // Nothing by default + } + + /** + * Automatically executed after the controller action. Can be used to apply + * transformation to the request response, add extra output, and execute + * other custom code. + * + * @return void + */ + public function after() + { + // Nothing by default + } + +} // End Controller diff --git a/includes/kohana/system/classes/kohana/controller/rest.php b/includes/kohana/system/classes/kohana/controller/rest.php new file mode 100644 index 00000000..829982ba --- /dev/null +++ b/includes/kohana/system/classes/kohana/controller/rest.php @@ -0,0 +1,74 @@ + 'index', + 'PUT' => 'update', + 'POST' => 'create', + 'DELETE' => 'delete', + ); + + protected $_action_requested = ''; + + /** + * Checks the requested method against the available methods. If the method + * is supported, sets the request action from the map. If not supported, + * the "invalid" action will be called. + */ + public function before() + { + $this->_action_requested = $this->request->action; + + if ( ! isset($this->_action_map[Request::$method])) + { + $this->request->action = 'invalid'; + } + else + { + $this->request->action = $this->_action_map[Request::$method]; + } + + return parent::before(); + } + + /** + * Sends a 405 "Method Not Allowed" response and a list of allowed actions. + */ + public function action_invalid() + { + // Send the "Method Not Allowed" response + $this->request->status = 405; + $this->request->headers['Allow'] = implode(', ', array_keys($this->_action_map)); + } + +} // End REST diff --git a/includes/kohana/system/classes/kohana/controller/template.php b/includes/kohana/system/classes/kohana/controller/template.php new file mode 100644 index 00000000..1d23a5b1 --- /dev/null +++ b/includes/kohana/system/classes/kohana/controller/template.php @@ -0,0 +1,50 @@ +auto_render === TRUE) + { + // Load the template + $this->template = View::factory($this->template); + } + + return parent::before(); + } + + /** + * Assigns the template [View] as the request response. + */ + public function after() + { + if ($this->auto_render === TRUE) + { + $this->request->response = $this->template; + } + + return parent::after(); + } + +} // End Controller_Template diff --git a/includes/kohana/system/classes/kohana/cookie.php b/includes/kohana/system/classes/kohana/cookie.php new file mode 100644 index 00000000..209495cb --- /dev/null +++ b/includes/kohana/system/classes/kohana/cookie.php @@ -0,0 +1,155 @@ + human readable name + */ + public static $php_errors = array( + E_ERROR => 'Fatal Error', + E_USER_ERROR => 'User Error', + E_PARSE => 'Parse Error', + E_WARNING => 'Warning', + E_USER_WARNING => 'User Warning', + E_STRICT => 'Strict', + E_NOTICE => 'Notice', + E_RECOVERABLE_ERROR => 'Recoverable Error', + ); + + /** + * @var string current environment name + */ + public static $environment = Kohana::DEVELOPMENT; + + /** + * @var boolean command line environment? + */ + public static $is_cli = FALSE; + + /** + * @var boolean Windows environment? + */ + public static $is_windows = FALSE; + + /** + * @var boolean magic quotes enabled? + */ + public static $magic_quotes = FALSE; + + /** + * @var boolean log errors and exceptions? + */ + public static $log_errors = FALSE; + + /** + * @var string character set of input and output + */ + public static $charset = 'utf-8'; + + /** + * @var string base URL to the application + */ + public static $base_url = '/'; + + /** + * @var string application index file + */ + public static $index_file = 'index.php'; + + /** + * @var string cache directory + */ + public static $cache_dir; + + /** + * @var integer default lifetime for caching, in seconds + */ + public static $cache_life = 60; + + /** + * @var boolean enabling internal caching? + */ + public static $caching = FALSE; + + /** + * @var boolean enable core profiling? + */ + public static $profiling = TRUE; + + /** + * @var boolean enable error handling? + */ + public static $errors = TRUE; + + /** + * @var string error rendering view + */ + public static $error_view = 'kohana/error'; + + /** + * @var array types of errors to display at shutdown + */ + public static $shutdown_errors = array(E_PARSE, E_ERROR, E_USER_ERROR, E_COMPILE_ERROR); + + /** + * @var object logging object + */ + public static $log; + + /** + * @var object config object + */ + public static $config; + + // Is the environment initialized? + protected static $_init = FALSE; + + // Currently active modules + protected static $_modules = array(); + + // Include paths that are used to find files + protected static $_paths = array(APPPATH, SYSPATH); + + // File path cache + protected static $_files = array(); + + // Has the file cache changed? + protected static $_files_changed = FALSE; + + /** + * Initializes the environment: + * + * - Disables register_globals and magic_quotes_gpc + * - Determines the current environment + * - Set global settings + * - Sanitizes GET, POST, and COOKIE variables + * - Converts GET, POST, and COOKIE variables to the global character set + * + * Any of the global settings can be set here: + * + * Type | Setting | Description | Default Value + * ----------|------------|------------------------------------------------|--------------- + * `boolean` | errors | use internal error and exception handling? | `TRUE` + * `boolean` | profile | do internal benchmarking? | `TRUE` + * `boolean` | caching | cache the location of files between requests? | `FALSE` + * `string` | charset | character set used for all input and output | `"utf-8"` + * `string` | base_url | set the base URL for the application | `"/"` + * `string` | index_file | set the index.php file name | `"index.php"` + * `string` | cache_dir | set the cache directory path | `APPPATH."cache"` + * `integer` | cache_life | set the default cache lifetime | `60` + * `string` | error_view | set the error rendering view | `"kohana/error"` + * + * @throws Kohana_Exception + * @param array global settings + * @return void + * @uses Kohana::globals + * @uses Kohana::sanitize + * @uses Kohana::cache + * @uses Profiler + */ + public static function init(array $settings = NULL) + { + if (Kohana::$_init) + { + // Do not allow execution twice + return; + } + + // Kohana is now initialized + Kohana::$_init = TRUE; + + if (isset($settings['profile'])) + { + // Enable profiling + Kohana::$profiling = (bool) $settings['profile']; + } + + // Start an output buffer + ob_start(); + + if (defined('E_DEPRECATED')) + { + // E_DEPRECATED only exists in PHP >= 5.3.0 + Kohana::$php_errors[E_DEPRECATED] = 'Deprecated'; + } + + if (isset($settings['errors'])) + { + // Enable error handling + Kohana::$errors = (bool) $settings['errors']; + } + + if (Kohana::$errors === TRUE) + { + // Enable Kohana exception handling, adds stack traces and error source. + set_exception_handler(array('Kohana', 'exception_handler')); + + // Enable Kohana error handling, converts all PHP errors to exceptions. + set_error_handler(array('Kohana', 'error_handler')); + } + + // Enable the Kohana shutdown handler, which catches E_FATAL errors. + register_shutdown_function(array('Kohana', 'shutdown_handler')); + + if (isset($settings['error_view'])) + { + if ( ! Kohana::find_file('views', $settings['error_view'])) + { + throw new Kohana_Exception('Error view file does not exist: views/:file', array( + ':file' => $settings['error_view'], + )); + } + + // Change the default error rendering + Kohana::$error_view = (string) $settings['error_view']; + } + + if (ini_get('register_globals')) + { + // Reverse the effects of register_globals + Kohana::globals(); + } + + // Determine if we are running in a command line environment + Kohana::$is_cli = (PHP_SAPI === 'cli'); + + // Determine if we are running in a Windows environment + Kohana::$is_windows = (DIRECTORY_SEPARATOR === '\\'); + + if (isset($settings['cache_dir'])) + { + if ( ! is_dir($settings['cache_dir'])) + { + try + { + // Create the cache directory + mkdir($settings['cache_dir'], 0755, TRUE); + + // Set permissions (must be manually set to fix umask issues) + chmod($settings['cache_dir'], 0755); + } + catch (Exception $e) + { + throw new Kohana_Exception('Could not create cache directory :dir', + array(':dir' => Kohana::debug_path($settings['cache_dir']))); + } + } + + // Set the cache directory path + Kohana::$cache_dir = realpath($settings['cache_dir']); + } + else + { + // Use the default cache directory + Kohana::$cache_dir = APPPATH.'cache'; + } + + if ( ! is_writable(Kohana::$cache_dir)) + { + throw new Kohana_Exception('Directory :dir must be writable', + array(':dir' => Kohana::debug_path(Kohana::$cache_dir))); + } + + if (isset($settings['cache_life'])) + { + // Set the default cache lifetime + Kohana::$cache_life = (int) $settings['cache_life']; + } + + if (isset($settings['caching'])) + { + // Enable or disable internal caching + Kohana::$caching = (bool) $settings['caching']; + } + + if (Kohana::$caching === TRUE) + { + // Load the file path cache + Kohana::$_files = Kohana::cache('Kohana::find_file()'); + } + + if (isset($settings['charset'])) + { + // Set the system character set + Kohana::$charset = strtolower($settings['charset']); + } + + if (function_exists('mb_internal_encoding')) + { + // Set the MB extension encoding to the same character set + mb_internal_encoding(Kohana::$charset); + } + + if (isset($settings['base_url'])) + { + // Set the base URL + Kohana::$base_url = rtrim($settings['base_url'], '/').'/'; + } + + if (isset($settings['index_file'])) + { + // Set the index file + Kohana::$index_file = trim($settings['index_file'], '/'); + } + + // Determine if the extremely evil magic quotes are enabled + Kohana::$magic_quotes = (bool) get_magic_quotes_gpc(); + + // Sanitize all request variables + $_GET = Kohana::sanitize($_GET); + $_POST = Kohana::sanitize($_POST); + $_COOKIE = Kohana::sanitize($_COOKIE); + + // Load the logger + Kohana::$log = Kohana_Log::instance(); + + // Load the config + Kohana::$config = Kohana_Config::instance(); + } + + /** + * Cleans up the environment: + * + * - Restore the previous error and exception handlers + * - Destroy the Kohana::$log and Kohana::$config objects + * + * @return void + */ + public static function deinit() + { + if (Kohana::$_init) + { + // Removed the autoloader + spl_autoload_unregister(array('Kohana', 'auto_load')); + + if (Kohana::$errors) + { + // Go back to the previous error handler + restore_error_handler(); + + // Go back to the previous exception handler + restore_exception_handler(); + } + + // Destroy objects created by init + Kohana::$log = Kohana::$config = NULL; + + // Reset internal storage + Kohana::$_modules = Kohana::$_files = array(); + Kohana::$_paths = array(APPPATH, SYSPATH); + + // Reset file cache status + Kohana::$_files_changed = FALSE; + + // Kohana is no longer initialized + Kohana::$_init = FALSE; + } + } + + /** + * Reverts the effects of the `register_globals` PHP setting by unsetting + * all global varibles except for the default super globals (GPCS, etc). + * + * if (ini_get('register_globals')) + * { + * Kohana::globals(); + * } + * + * @return void + */ + public static function globals() + { + if (isset($_REQUEST['GLOBALS']) OR isset($_FILES['GLOBALS'])) + { + // Prevent malicious GLOBALS overload attack + echo "Global variable overload attack detected! Request aborted.\n"; + + // Exit with an error status + exit(1); + } + + // Get the variable names of all globals + $global_variables = array_keys($GLOBALS); + + // Remove the standard global variables from the list + $global_variables = array_diff($global_variables, array( + '_COOKIE', + '_ENV', + '_GET', + '_FILES', + '_POST', + '_REQUEST', + '_SERVER', + '_SESSION', + 'GLOBALS', + )); + + foreach ($global_variables as $name) + { + // Unset the global variable, effectively disabling register_globals + unset($GLOBALS[$name]); + } + } + + /** + * Recursively sanitizes an input variable: + * + * - Strips slashes if magic quotes are enabled + * - Normalizes all newlines to LF + * + * @param mixed any variable + * @return mixed sanitized variable + */ + public static function sanitize($value) + { + if (is_array($value) OR is_object($value)) + { + foreach ($value as $key => $val) + { + // Recursively clean each value + $value[$key] = Kohana::sanitize($val); + } + } + elseif (is_string($value)) + { + if (Kohana::$magic_quotes === TRUE) + { + // Remove slashes added by magic quotes + $value = stripslashes($value); + } + + if (strpos($value, "\r") !== FALSE) + { + // Standardize newlines + $value = str_replace(array("\r\n", "\r"), "\n", $value); + } + } + + return $value; + } + + /** + * Provides auto-loading support of Kohana classes, as well as transparent + * extension of classes that have a _Core suffix. + * + * Class names are converted to file names by making the class name + * lowercase and converting underscores to slashes: + * + * // Loads classes/my/class/name.php + * Kohana::auto_load('My_Class_Name'); + * + * @param string class name + * @return boolean + */ + public static function auto_load($class) + { + // Transform the class name into a path + $file = str_replace('_', '/', strtolower($class)); + + if ($path = Kohana::find_file('classes', $file)) + { + // Load the class file + require $path; + + // Class has been found + return TRUE; + } + + // Class is not in the filesystem + return FALSE; + } + + /** + * Changes the currently enabled modules. Module paths may be relative + * or absolute, but must point to a directory: + * + * Kohana::modules(array('modules/foo', MODPATH.'bar')); + * + * @param array list of module paths + * @return array enabled modules + */ + public static function modules(array $modules = NULL) + { + if ($modules === NULL) + { + // Not changing modules, just return the current set + return Kohana::$_modules; + } + + // Start a new list of include paths, APPPATH first + $paths = array(APPPATH); + + foreach ($modules as $name => $path) + { + if (is_dir($path)) + { + // Add the module to include paths + $paths[] = $modules[$name] = realpath($path).DIRECTORY_SEPARATOR; + } + else + { + // This module is invalid, remove it + unset($modules[$name]); + } + } + + // Finish the include paths by adding SYSPATH + $paths[] = SYSPATH; + + // Set the new include paths + Kohana::$_paths = $paths; + + // Set the current module list + Kohana::$_modules = $modules; + + foreach (Kohana::$_modules as $path) + { + $init = $path.'init'.EXT; + + if (is_file($init)) + { + // Include the module initialization file once + require_once $init; + } + } + + return Kohana::$_modules; + } + + /** + * Returns the the currently active include paths, including the + * application and system paths. + * + * @return array + */ + public static function include_paths() + { + return Kohana::$_paths; + } + + /** + * Finds the path of a file by directory, filename, and extension. + * If no extension is given, the default EXT extension will be used. + * + * When searching the "config" or "i18n" directories, or when the + * $aggregate_files flag is set to true, an array of files + * will be returned. These files will return arrays which must be + * merged together. + * + * // Returns an absolute path to views/template.php + * Kohana::find_file('views', 'template'); + * + * // Returns an absolute path to media/css/style.css + * Kohana::find_file('media', 'css/style', 'css'); + * + * // Returns an array of all the "mimes" configuration file + * Kohana::find_file('config', 'mimes'); + * + * @param string directory name (views, i18n, classes, extensions, etc.) + * @param string filename with subdirectory + * @param string extension to search for + * @param boolean return an array of files? + * @return array a list of files when $array is TRUE + * @return string single file path + */ + public static function find_file($dir, $file, $ext = NULL, $array = FALSE) + { + if ($ext === NULL) + { + // Use the default extension + $ext = EXT; + } + elseif ($ext) + { + // Prefix the extension with a period + $ext = ".{$ext}"; + } + else + { + // Use no extension + $ext = ''; + } + + // Create a partial path of the filename + $path = $dir.DIRECTORY_SEPARATOR.$file.$ext; + + if (Kohana::$caching === TRUE AND isset(Kohana::$_files[$path])) + { + // This path has been cached + return Kohana::$_files[$path]; + } + + if (Kohana::$profiling === TRUE AND class_exists('Profiler', FALSE)) + { + // Start a new benchmark + $benchmark = Profiler::start('Kohana', __FUNCTION__); + } + + if ($array OR $dir === 'config' OR $dir === 'i18n' OR $dir === 'messages') + { + // Include paths must be searched in reverse + $paths = array_reverse(Kohana::$_paths); + + // Array of files that have been found + $found = array(); + + foreach ($paths as $dir) + { + if (is_file($dir.$path)) + { + // This path has a file, add it to the list + $found[] = $dir.$path; + } + } + } + else + { + // The file has not been found yet + $found = FALSE; + + foreach (Kohana::$_paths as $dir) + { + if (is_file($dir.$path)) + { + // A path has been found + $found = $dir.$path; + + // Stop searching + break; + } + } + } + + if (Kohana::$caching === TRUE) + { + // Add the path to the cache + Kohana::$_files[$path] = $found; + + // Files have been changed + Kohana::$_files_changed = TRUE; + } + + if (isset($benchmark)) + { + // Stop the benchmark + Profiler::stop($benchmark); + } + + return $found; + } + + /** + * Recursively finds all of the files in the specified directory. + * + * $views = Kohana::list_files('views'); + * + * @param string directory name + * @param array list of paths to search + * @return array + */ + public static function list_files($directory = NULL, array $paths = NULL) + { + if ($directory !== NULL) + { + // Add the directory separator + $directory .= DIRECTORY_SEPARATOR; + } + + if ($paths === NULL) + { + // Use the default paths + $paths = Kohana::$_paths; + } + + // Create an array for the files + $found = array(); + + foreach ($paths as $path) + { + if (is_dir($path.$directory)) + { + // Create a new directory iterator + $dir = new DirectoryIterator($path.$directory); + + foreach ($dir as $file) + { + // Get the file name + $filename = $file->getFilename(); + + if ($filename[0] === '.' OR $filename[strlen($filename)-1] === '~') + { + // Skip all hidden files and UNIX backup files + continue; + } + + // Relative filename is the array key + $key = $directory.$filename; + + if ($file->isDir()) + { + if ($sub_dir = Kohana::list_files($key, $paths)) + { + if (isset($found[$key])) + { + // Append the sub-directory list + $found[$key] += $sub_dir; + } + else + { + // Create a new sub-directory list + $found[$key] = $sub_dir; + } + } + } + else + { + if ( ! isset($found[$key])) + { + // Add new files to the list + $found[$key] = realpath($file->getPathName()); + } + } + } + } + } + + // Sort the results alphabetically + ksort($found); + + return $found; + } + + /** + * Loads a file within a totally empty scope and returns the output: + * + * $foo = Kohana::load('foo.php'); + * + * @param string + * @return mixed + */ + public static function load($file) + { + return include $file; + } + + /** + * Creates a new configuration object for the requested group. + * + * @param string group name + * @return Kohana_Config + */ + public static function config($group) + { + static $config; + + if (strpos($group, '.') !== FALSE) + { + // Split the config group and path + list ($group, $path) = explode('.', $group, 2); + } + + if ( ! isset($config[$group])) + { + // Load the config group into the cache + $config[$group] = Kohana::$config->load($group); + } + + if (isset($path)) + { + return Arr::path($config[$group], $path, NULL, '.'); + } + else + { + return $config[$group]; + } + } + + /** + * Provides simple file-based caching for strings and arrays: + * + * // Set the "foo" cache + * Kohana::cache('foo', 'hello, world'); + * + * // Get the "foo" cache + * $foo = Kohana::cache('foo'); + * + * All caches are stored as PHP code, generated with [var_export][ref-var]. + * Caching objects may not work as expected. Storing references or an + * object or array that has recursion will cause an E_FATAL. + * + * [ref-var]: http://php.net/var_export + * + * @throws Kohana_Exception + * @param string name of the cache + * @param mixed data to cache + * @param integer number of seconds the cache is valid for + * @return mixed for getting + * @return boolean for setting + */ + public static function cache($name, $data = NULL, $lifetime = NULL) + { + // Cache file is a hash of the name + $file = sha1($name).'.txt'; + + // Cache directories are split by keys to prevent filesystem overload + $dir = Kohana::$cache_dir.DIRECTORY_SEPARATOR.$file[0].$file[1].DIRECTORY_SEPARATOR; + + if ($lifetime === NULL) + { + // Use the default lifetime + $lifetime = Kohana::$cache_life; + } + + if ($data === NULL) + { + if (is_file($dir.$file)) + { + if ((time() - filemtime($dir.$file)) < $lifetime) + { + // Return the cache + return unserialize(file_get_contents($dir.$file)); + } + else + { + try + { + // Cache has expired + unlink($dir.$file); + } + catch (Exception $e) + { + // Cache has mostly likely already been deleted, + // let return happen normally. + } + } + } + + // Cache not found + return NULL; + } + + if ( ! is_dir($dir)) + { + // Create the cache directory + mkdir($dir, 0777, TRUE); + + // Set permissions (must be manually set to fix umask issues) + chmod($dir, 0777); + } + + // Force the data to be a string + $data = serialize($data); + + try + { + // Write the cache + return (bool) file_put_contents($dir.$file, $data, LOCK_EX); + } + catch (Exception $e) + { + // Failed to write cache + return FALSE; + } + } + + /** + * Get a message from a file. Messages are arbitary strings that are stored + * in the messages/ directory and reference by a key. Translation is not + * performed on the returned values. + * + * // Get "username" from messages/text.php + * $username = Kohana::message('text', 'username'); + * + * @param string file name + * @param string key path to get + * @param mixed default value if the path does not exist + * @return string message string for the given path + * @return array complete message list, when no path is specified + * @uses Arr::merge + * @uses Arr::path + */ + public static function message($file, $path = NULL, $default = NULL) + { + static $messages; + + if ( ! isset($messages[$file])) + { + // Create a new message list + $messages[$file] = array(); + + if ($files = Kohana::find_file('messages', $file)) + { + foreach ($files as $f) + { + // Combine all the messages recursively + $messages[$file] = Arr::merge($messages[$file], Kohana::load($f)); + } + } + } + + if ($path === NULL) + { + // Return all of the messages + return $messages[$file]; + } + else + { + // Get a message using the path + return Arr::path($messages[$file], $path, $default); + } + } + + /** + * PHP error handler, converts all errors into ErrorExceptions. This handler + * respects error_reporting settings. + * + * @throws ErrorException + * @return TRUE + */ + public static function error_handler($code, $error, $file = NULL, $line = NULL) + { + if (error_reporting() & $code) + { + // This error is not suppressed by current error reporting settings + // Convert the error into an ErrorException + throw new ErrorException($error, $code, 0, $file, $line); + } + + // Do not execute the PHP error handler + return TRUE; + } + + /** + * Inline exception handler, displays the error message, source of the + * exception, and the stack trace of the error. + * + * @uses Kohana::exception_text + * @param object exception object + * @return boolean + */ + public static function exception_handler(Exception $e) + { + try + { + // Get the exception information + $type = get_class($e); + $code = $e->getCode(); + $message = $e->getMessage(); + $file = $e->getFile(); + $line = $e->getLine(); + + // Create a text version of the exception + $error = Kohana::exception_text($e); + + if (is_object(Kohana::$log)) + { + // Add this exception to the log + Kohana::$log->add(Kohana::ERROR, $error); + + // Make sure the logs are written + Kohana::$log->write(); + } + + if (Kohana::$is_cli) + { + // Just display the text of the exception + echo "\n{$error}\n"; + + return TRUE; + } + + // Get the exception backtrace + $trace = $e->getTrace(); + + if ($e instanceof ErrorException) + { + if (isset(Kohana::$php_errors[$code])) + { + // Use the human-readable error name + $code = Kohana::$php_errors[$code]; + } + + if (version_compare(PHP_VERSION, '5.3', '<')) + { + // Workaround for a bug in ErrorException::getTrace() that exists in + // all PHP 5.2 versions. @see http://bugs.php.net/bug.php?id=45895 + for ($i = count($trace) - 1; $i > 0; --$i) + { + if (isset($trace[$i - 1]['args'])) + { + // Re-position the args + $trace[$i]['args'] = $trace[$i - 1]['args']; + + // Remove the args + unset($trace[$i - 1]['args']); + } + } + } + } + + if ( ! headers_sent()) + { + // Make sure the proper content type is sent with a 500 status + header('Content-Type: text/html; charset='.Kohana::$charset, TRUE, 500); + } + + // Start an output buffer + ob_start(); + + // Include the exception HTML + include Kohana::find_file('views', Kohana::$error_view); + + // Display the contents of the output buffer + echo ob_get_clean(); + + return TRUE; + } + catch (Exception $e) + { + // Clean the output buffer if one exists + ob_get_level() and ob_clean(); + + // Display the exception text + echo Kohana::exception_text($e), "\n"; + + // Exit with an error status + // exit(1); + } + } + + /** + * Catches errors that are not caught by the error handler, such as E_PARSE. + * + * @uses Kohana::exception_handler + * @return void + */ + public static function shutdown_handler() + { + if ( ! Kohana::$_init) + { + // Do not execute when not active + return; + } + + try + { + if (Kohana::$caching === TRUE AND Kohana::$_files_changed === TRUE) + { + // Write the file path cache + Kohana::cache('Kohana::find_file()', Kohana::$_files); + } + } + catch (Exception $e) + { + // Pass the exception to the handler + Kohana::exception_handler($e); + } + + if (Kohana::$errors AND $error = error_get_last() AND in_array($error['type'], Kohana::$shutdown_errors)) + { + // Clean the output buffer + ob_get_level() and ob_clean(); + + // Fake an exception for nice debugging + Kohana::exception_handler(new ErrorException($error['message'], $error['type'], 0, $error['file'], $error['line'])); + + // Shutdown now to avoid a "death loop" + exit(1); + } + } + + /** + * Get a single line of text representing the exception: + * + * Error [ Code ]: Message ~ File [ Line ] + * + * @param object Exception + * @return string + */ + public static function exception_text(Exception $e) + { + return sprintf('%s [ %s ]: %s ~ %s [ %d ]', + get_class($e), $e->getCode(), strip_tags($e->getMessage()), Kohana::debug_path($e->getFile()), $e->getLine()); + } + + /** + * Returns an HTML string of debugging information about any number of + * variables, each wrapped in a "pre" tag: + * + * // Displays the type and value of each variable + * echo Kohana::debug($foo, $bar, $baz); + * + * @param mixed variable to debug + * @param ... + * @return string + */ + public static function debug() + { + if (func_num_args() === 0) + return; + + // Get all passed variables + $variables = func_get_args(); + + $output = array(); + foreach ($variables as $var) + { + $output[] = Kohana::_dump($var, 1024); + } + + return '
            '.implode("\n", $output).'
            '; + } + + /** + * Returns an HTML string of information about a single variable. + * + * Borrows heavily on concepts from the Debug class of [Nette](http://nettephp.com/). + * + * @param mixed variable to dump + * @param integer maximum length of strings + * @return string + */ + public static function dump($value, $length = 128) + { + return Kohana::_dump($value, $length); + } + + /** + * Helper for Kohana::dump(), handles recursion in arrays and objects. + * + * @param mixed variable to dump + * @param integer maximum length of strings + * @param integer recursion level (internal) + * @return string + */ + protected static function _dump( & $var, $length = 128, $level = 0) + { + if ($var === NULL) + { + return 'NULL'; + } + elseif (is_bool($var)) + { + return 'bool '.($var ? 'TRUE' : 'FALSE'); + } + elseif (is_float($var)) + { + return 'float '.$var; + } + elseif (is_resource($var)) + { + if (($type = get_resource_type($var)) === 'stream' AND $meta = stream_get_meta_data($var)) + { + $meta = stream_get_meta_data($var); + + if (isset($meta['uri'])) + { + $file = $meta['uri']; + + if (function_exists('stream_is_local')) + { + // Only exists on PHP >= 5.2.4 + if (stream_is_local($file)) + { + $file = Kohana::debug_path($file); + } + } + + return 'resource('.$type.') '.htmlspecialchars($file, ENT_NOQUOTES, Kohana::$charset); + } + } + else + { + return 'resource('.$type.')'; + } + } + elseif (is_string($var)) + { + // Clean invalid multibyte characters. iconv is only invoked + // if there are non ASCII characters in the string, so this + // isn't too much of a hit. + $var = UTF8::clean($var); + + if (UTF8::strlen($var) > $length) + { + // Encode the truncated string + $str = htmlspecialchars(UTF8::substr($var, 0, $length), ENT_NOQUOTES, Kohana::$charset).' …'; + } + else + { + // Encode the string + $str = htmlspecialchars($var, ENT_NOQUOTES, Kohana::$charset); + } + + return 'string('.strlen($var).') "'.$str.'"'; + } + elseif (is_array($var)) + { + $output = array(); + + // Indentation for this variable + $space = str_repeat($s = ' ', $level); + + static $marker; + + if ($marker === NULL) + { + // Make a unique marker + $marker = uniqid("\x00"); + } + + if (empty($var)) + { + // Do nothing + } + elseif (isset($var[$marker])) + { + $output[] = "(\n$space$s*RECURSION*\n$space)"; + } + elseif ($level < 5) + { + $output[] = "("; + + $var[$marker] = TRUE; + foreach ($var as $key => & $val) + { + if ($key === $marker) continue; + if ( ! is_int($key)) + { + $key = '"'.htmlspecialchars($key, ENT_NOQUOTES, self::$charset).'"'; + } + + $output[] = "$space$s$key => ".Kohana::_dump($val, $length, $level + 1); + } + unset($var[$marker]); + + $output[] = "$space)"; + } + else + { + // Depth too great + $output[] = "(\n$space$s...\n$space)"; + } + + return 'array('.count($var).') '.implode("\n", $output); + } + elseif (is_object($var)) + { + // Copy the object as an array + $array = (array) $var; + + $output = array(); + + // Indentation for this variable + $space = str_repeat($s = ' ', $level); + + $hash = spl_object_hash($var); + + // Objects that are being dumped + static $objects = array(); + + if (empty($var)) + { + // Do nothing + } + elseif (isset($objects[$hash])) + { + $output[] = "{\n$space$s*RECURSION*\n$space}"; + } + elseif ($level < 10) + { + $output[] = "{"; + + $objects[$hash] = TRUE; + foreach ($array as $key => & $val) + { + if ($key[0] === "\x00") + { + // Determine if the access is protected or protected + $access = ''.($key[1] === '*' ? 'protected' : 'private').''; + + // Remove the access level from the variable name + $key = substr($key, strrpos($key, "\x00") + 1); + } + else + { + $access = 'public'; + } + + $output[] = "$space$s$access $key => ".Kohana::_dump($val, $length, $level + 1); + } + unset($objects[$hash]); + + $output[] = "$space}"; + } + else + { + // Depth too great + $output[] = "{\n$space$s...\n$space}"; + } + + return 'object '.get_class($var).'('.count($array).') '.implode("\n", $output); + } + else + { + return ''.gettype($var).' '.htmlspecialchars(print_r($var, TRUE), ENT_NOQUOTES, Kohana::$charset); + } + } + + /** + * Removes application, system, modpath, or docroot from a filename, + * replacing them with the plain text equivalents. Useful for debugging + * when you want to display a shorter path. + * + * // Displays SYSPATH/classes/kohana.php + * echo Kohana::debug_path(Kohana::find_file('classes', 'kohana')); + * + * @param string path to debug + * @return string + */ + public static function debug_path($file) + { + if (strpos($file, APPPATH) === 0) + { + $file = 'APPPATH'.DIRECTORY_SEPARATOR.substr($file, strlen(APPPATH)); + } + elseif (strpos($file, SYSPATH) === 0) + { + $file = 'SYSPATH'.DIRECTORY_SEPARATOR.substr($file, strlen(SYSPATH)); + } + elseif (strpos($file, MODPATH) === 0) + { + $file = 'MODPATH'.DIRECTORY_SEPARATOR.substr($file, strlen(MODPATH)); + } + elseif (strpos($file, DOCROOT) === 0) + { + $file = 'DOCROOT'.DIRECTORY_SEPARATOR.substr($file, strlen(DOCROOT)); + } + + return $file; + } + + /** + * Returns an HTML string, highlighting a specific line of a file, with some + * number of lines padded above and below. + * + * // Highlights the current line of the current file + * echo Kohana::debug_source(__FILE__, __LINE__); + * + * @param string file to open + * @param integer line number to highlight + * @param integer number of padding lines + * @return string source of file + * @return FALSE file is unreadable + */ + public static function debug_source($file, $line_number, $padding = 5) + { + if ( ! $file OR ! is_readable($file)) + { + // Continuing will cause errors + return FALSE; + } + + // Open the file and set the line position + $file = fopen($file, 'r'); + $line = 0; + + // Set the reading range + $range = array('start' => $line_number - $padding, 'end' => $line_number + $padding); + + // Set the zero-padding amount for line numbers + $format = '% '.strlen($range['end']).'d'; + + $source = ''; + while (($row = fgets($file)) !== FALSE) + { + // Increment the line number + if (++$line > $range['end']) + break; + + if ($line >= $range['start']) + { + // Make the row safe for output + $row = htmlspecialchars($row, ENT_NOQUOTES, Kohana::$charset); + + // Trim whitespace and sanitize the row + $row = ''.sprintf($format, $line).' '.$row; + + if ($line === $line_number) + { + // Apply highlighting to this row + $row = ''.$row.''; + } + else + { + $row = ''.$row.''; + } + + // Add to the captured source + $source .= $row; + } + } + + // Close the file + fclose($file); + + return '
            '.$source.'
            '; + } + + /** + * Returns an array of HTML strings that represent each step in the backtrace. + * + * // Displays the entire current backtrace + * echo implode('
            ', Kohana::trace()); + * + * @param string path to debug + * @return string + */ + public static function trace(array $trace = NULL) + { + if ($trace === NULL) + { + // Start a new trace + $trace = debug_backtrace(); + } + + // Non-standard function calls + $statements = array('include', 'include_once', 'require', 'require_once'); + + $output = array(); + foreach ($trace as $step) + { + if ( ! isset($step['function'])) + { + // Invalid trace step + continue; + } + + if (isset($step['file']) AND isset($step['line'])) + { + // Include the source of this step + $source = Kohana::debug_source($step['file'], $step['line']); + } + + if (isset($step['file'])) + { + $file = $step['file']; + + if (isset($step['line'])) + { + $line = $step['line']; + } + } + + // function() + $function = $step['function']; + + if (in_array($step['function'], $statements)) + { + if (empty($step['args'])) + { + // No arguments + $args = array(); + } + else + { + // Sanitize the file path + $args = array($step['args'][0]); + } + } + elseif (isset($step['args'])) + { + if ( ! function_exists($step['function']) OR strpos($step['function'], '{closure}') !== FALSE) + { + // Introspection on closures or language constructs in a stack trace is impossible + $params = NULL; + } + else + { + if (isset($step['class'])) + { + if (method_exists($step['class'], $step['function'])) + { + $reflection = new ReflectionMethod($step['class'], $step['function']); + } + else + { + $reflection = new ReflectionMethod($step['class'], '__call'); + } + } + else + { + $reflection = new ReflectionFunction($step['function']); + } + + // Get the function parameters + $params = $reflection->getParameters(); + } + + $args = array(); + + foreach ($step['args'] as $i => $arg) + { + if (isset($params[$i])) + { + // Assign the argument by the parameter name + $args[$params[$i]->name] = $arg; + } + else + { + // Assign the argument by number + $args[$i] = $arg; + } + } + } + + if (isset($step['class'])) + { + // Class->method() or Class::method() + $function = $step['class'].$step['type'].$step['function']; + } + + $output[] = array( + 'function' => $function, + 'args' => isset($args) ? $args : NULL, + 'file' => isset($file) ? $file : NULL, + 'line' => isset($line) ? $line : NULL, + 'source' => isset($source) ? $source : NULL, + ); + + unset($function, $args, $file, $line, $source); + } + + return $output; + } + +} // End Kohana diff --git a/includes/kohana/system/classes/kohana/date.php b/includes/kohana/system/classes/kohana/date.php new file mode 100644 index 00000000..af9745e2 --- /dev/null +++ b/includes/kohana/system/classes/kohana/date.php @@ -0,0 +1,557 @@ +. + * + * @param string timezone that to find the offset of + * @param string timezone used as the baseline + * @param mixed UNIX timestamp or date string + * @return integer + */ + public static function offset($remote, $local = NULL, $now = NULL) + { + if ($local === NULL) + { + // Use the default timezone + $local = date_default_timezone_get(); + } + + if (is_int($now)) + { + // Convert the timestamp into a string + $now = date(DateTime::RFC2822, $now); + } + + // Create timezone objects + $zone_remote = new DateTimeZone($remote); + $zone_local = new DateTimeZone($local); + + // Create date objects from timezones + $time_remote = new DateTime($now, $zone_remote); + $time_local = new DateTime($now, $zone_local); + + // Find the offset + $offset = $zone_remote->getOffset($time_remote) - $zone_local->getOffset($time_local); + + return $offset; + } + + /** + * Number of seconds in a minute, incrementing by a step. Typically used as + * a shortcut for generating a list that can used in a form. + * + * $seconds = Date::seconds(); // 01, 02, 03, ..., 58, 59, 60 + * + * @param integer amount to increment each step by, 1 to 30 + * @param integer start value + * @param integer end value + * @return array A mirrored (foo => foo) array from 1-60. + */ + public static function seconds($step = 1, $start = 0, $end = 60) + { + // Always integer + $step = (int) $step; + + $seconds = array(); + + for ($i = $start; $i < $end; $i += $step) + { + $seconds[$i] = sprintf('%02d', $i); + } + + return $seconds; + } + + /** + * Number of minutes in an hour, incrementing by a step. Typically used as + * a shortcut for generating a list that can be used in a form. + * + * $minutes = Date::minutes(); // 05, 10, 15, ..., 50, 55, 60 + * + * @uses Date::seconds + * @param integer amount to increment each step by, 1 to 30 + * @return array A mirrored (foo => foo) array from 1-60. + */ + public static function minutes($step = 5) + { + // Because there are the same number of minutes as seconds in this set, + // we choose to re-use seconds(), rather than creating an entirely new + // function. Shhhh, it's cheating! ;) There are several more of these + // in the following methods. + return Date::seconds($step); + } + + /** + * Number of hours in a day. Typically used as a shortcut for generating a + * list that can be used in a form. + * + * $hours = Date::hours(); // 01, 02, 03, ..., 10, 11, 12 + * + * @param integer amount to increment each step by + * @param boolean use 24-hour time + * @param integer the hour to start at + * @return array A mirrored (foo => foo) array from start-12 or start-23. + */ + public static function hours($step = 1, $long = FALSE, $start = NULL) + { + // Default values + $step = (int) $step; + $long = (bool) $long; + $hours = array(); + + // Set the default start if none was specified. + if ($start === NULL) + { + $start = ($long === FALSE) ? 1 : 0; + } + + $hours = array(); + + // 24-hour time has 24 hours, instead of 12 + $size = ($long === TRUE) ? 23 : 12; + + for ($i = $start; $i <= $size; $i += $step) + { + $hours[$i] = (string) $i; + } + + return $hours; + } + + /** + * Returns AM or PM, based on a given hour (in 24 hour format). + * + * $type = Date::ampm(12); // PM + * $type = Date::ampm(1); // AM + * + * @param integer number of the hour + * @return string + */ + public static function ampm($hour) + { + // Always integer + $hour = (int) $hour; + + return ($hour > 11) ? 'PM' : 'AM'; + } + + /** + * Adjusts a non-24-hour number into a 24-hour number. + * + * $hour = Date::adjust(3, 'pm'); // 15 + * + * @param integer hour to adjust + * @param string AM or PM + * @return string + */ + public static function adjust($hour, $ampm) + { + $hour = (int) $hour; + $ampm = strtolower($ampm); + + switch ($ampm) + { + case 'am': + if ($hour == 12) + $hour = 0; + break; + case 'pm': + if ($hour < 12) + $hour += 12; + break; + } + + return sprintf('%02d', $hour); + } + + /** + * Number of days in a given month and year. Typically used as a shortcut + * for generating a list that can be used in a form. + * + * Date::days(4, 2010); // 1, 2, 3, ..., 28, 29, 30 + * + * @param integer number of month + * @param integer number of year to check month, defaults to the current year + * @return array A mirrored (foo => foo) array of the days. + */ + public static function days($month, $year = FALSE) + { + static $months; + + if ($year === FALSE) + { + // Use the current year by default + $year = date('Y'); + } + + // Always integers + $month = (int) $month; + $year = (int) $year; + + // We use caching for months, because time functions are used + if (empty($months[$year][$month])) + { + $months[$year][$month] = array(); + + // Use date to find the number of days in the given month + $total = date('t', mktime(1, 0, 0, $month, 1, $year)) + 1; + + for ($i = 1; $i < $total; $i++) + { + $months[$year][$month][$i] = (string) $i; + } + } + + return $months[$year][$month]; + } + + /** + * Number of months in a year. Typically used as a shortcut for generating + * a list that can be used in a form. + * + * Date::months(); // 01, 02, 03, ..., 10, 11, 12 + * + * @uses Date::hours + * @return array A mirrored (foo => foo) array from 1-12. + */ + public static function months() + { + return Date::hours(); + } + + /** + * Returns an array of years between a starting and ending year. By default, + * the the current year - 5 and current year + 5 will be used. Typically used + * as a shortcut for generating a list that can be used in a form. + * + * $years = Date::years(2000, 2010); // 2000, 2001, ..., 2009, 2010 + * + * @param integer starting year (default is current year - 5) + * @param integer ending year (default is current year + 5) + * @return array + */ + public static function years($start = FALSE, $end = FALSE) + { + // Default values + $start = ($start === FALSE) ? date('Y') - 5 : (int) $start; + $end = ($end === FALSE) ? date('Y') + 5 : (int) $end; + + $years = array(); + + for ($i = $start; $i <= $end; $i++) + { + $years[$i] = (string) $i; + } + + return $years; + } + + /** + * Returns time difference between two timestamps, in human readable format. + * If the second timestamp is not given, the current time will be used. + * Also consider using [Date::fuzzy_span] when displaying a span. + * + * $span = Date::span(60, 182, 'minutes,seconds'); // array('minutes' => 2, 'seconds' => 2) + * $span = Date::span(60, 182, 'minutes'); // 2 + * + * @param integer timestamp to find the span of + * @param integer timestamp to use as the baseline + * @param string formatting string + * @return string when only a single output is requested + * @return array associative list of all outputs requested + */ + public static function span($remote, $local = NULL, $output = 'years,months,weeks,days,hours,minutes,seconds') + { + // Normalize output + $output = trim(strtolower((string) $output)); + + if ( ! $output) + { + // Invalid output + return FALSE; + } + + // Array with the output formats + $output = preg_split('/[^a-z]+/', $output); + + // Convert the list of outputs to an associative array + $output = array_combine($output, array_fill(0, count($output), 0)); + + // Make the output values into keys + extract(array_flip($output), EXTR_SKIP); + + if ($local === NULL) + { + // Calculate the span from the current time + $local = time(); + } + + // Calculate timespan (seconds) + $timespan = abs($remote - $local); + + if (isset($output['years'])) + { + $timespan -= Date::YEAR * ($output['years'] = (int) floor($timespan / Date::YEAR)); + } + + if (isset($output['months'])) + { + $timespan -= Date::MONTH * ($output['months'] = (int) floor($timespan / Date::MONTH)); + } + + if (isset($output['weeks'])) + { + $timespan -= Date::WEEK * ($output['weeks'] = (int) floor($timespan / Date::WEEK)); + } + + if (isset($output['days'])) + { + $timespan -= Date::DAY * ($output['days'] = (int) floor($timespan / Date::DAY)); + } + + if (isset($output['hours'])) + { + $timespan -= Date::HOUR * ($output['hours'] = (int) floor($timespan / Date::HOUR)); + } + + if (isset($output['minutes'])) + { + $timespan -= Date::MINUTE * ($output['minutes'] = (int) floor($timespan / Date::MINUTE)); + } + + // Seconds ago, 1 + if (isset($output['seconds'])) + { + $output['seconds'] = $timespan; + } + + if (count($output) === 1) + { + // Only a single output was requested, return it + return array_pop($output); + } + + // Return array + return $output; + } + + /** + * Returns the difference between a time and now in a "fuzzy" way. + * Note that unlike [Date::span], the "local" timestamp will always be the + * current time. Displaying a fuzzy time instead of a date is usually + * faster to read and understand. + * + * $span = Date::fuzzy_span(time() - 10); // "moments ago" + * $span = Date::fuzzy_span(time() + 20); // "in moments" + * + * @param integer "remote" timestamp + * @return string + */ + public static function fuzzy_span($timestamp) + { + // Determine the difference in seconds + $offset = abs(time() - $timestamp); + + if ($offset <= Date::MINUTE) + { + $span = 'moments'; + } + elseif ($offset < (Date::MINUTE * 20)) + { + $span = 'a few minutes'; + } + elseif ($offset < Date::HOUR) + { + $span = 'less than an hour'; + } + elseif ($offset < (Date::HOUR * 4)) + { + $span = 'a couple of hours'; + } + elseif ($offset < Date::DAY) + { + $span = 'less than a day'; + } + elseif ($offset < (Date::DAY * 2)) + { + $span = 'about a day'; + } + elseif ($offset < (Date::DAY * 4)) + { + $span = 'a couple of days'; + } + elseif ($offset < Date::WEEK) + { + $span = 'less than a week'; + } + elseif ($offset < (Date::WEEK * 2)) + { + $span = 'about a week'; + } + elseif ($offset < Date::MONTH) + { + $span = 'less than a month'; + } + elseif ($offset < (Date::MONTH * 2)) + { + $span = 'about a month'; + } + elseif ($offset < (Date::MONTH * 4)) + { + $span = 'a couple of months'; + } + elseif ($offset < Date::YEAR) + { + $span = 'less than a year'; + } + elseif ($offset < (Date::YEAR * 2)) + { + $span = 'about a year'; + } + elseif ($offset < (Date::YEAR * 4)) + { + $span = 'a couple of years'; + } + elseif ($offset < (Date::YEAR * 8)) + { + $span = 'a few years'; + } + elseif ($offset < (Date::YEAR * 12)) + { + $span = 'about a decade'; + } + elseif ($offset < (Date::YEAR * 24)) + { + $span = 'a couple of decades'; + } + elseif ($offset < (Date::YEAR * 64)) + { + $span = 'several decades'; + } + else + { + $span = 'a long time'; + } + + if ($timestamp <= time()) + { + // This is in the past + return $span.' ago'; + } + else + { + // This in the future + return 'in '.$span; + } + } + + /** + * Converts a UNIX timestamp to DOS format. There are very few cases where + * this is needed, but some binary formats use it (eg: zip files.) + * Converting the other direction is done using {@link Date::dos2unix}. + * + * $dos = Date::unix2dos($unix); + * + * @param integer UNIX timestamp + * @return integer + */ + public static function unix2dos($timestamp = FALSE) + { + $timestamp = ($timestamp === FALSE) ? getdate() : getdate($timestamp); + + if ($timestamp['year'] < 1980) + { + return (1 << 21 | 1 << 16); + } + + $timestamp['year'] -= 1980; + + // What voodoo is this? I have no idea... Geert can explain it though, + // and that's good enough for me. + return ($timestamp['year'] << 25 | $timestamp['mon'] << 21 | + $timestamp['mday'] << 16 | $timestamp['hours'] << 11 | + $timestamp['minutes'] << 5 | $timestamp['seconds'] >> 1); + } + + /** + * Converts a DOS timestamp to UNIX format.There are very few cases where + * this is needed, but some binary formats use it (eg: zip files.) + * Converting the other direction is done using {@link Date::unix2dos}. + * + * $unix = Date::dos2unix($dos); + * + * @param integer DOS timestamp + * @return integer + */ + public static function dos2unix($timestamp = FALSE) + { + $sec = 2 * ($timestamp & 0x1f); + $min = ($timestamp >> 5) & 0x3f; + $hrs = ($timestamp >> 11) & 0x1f; + $day = ($timestamp >> 16) & 0x1f; + $mon = ($timestamp >> 21) & 0x0f; + $year = ($timestamp >> 25) & 0x7f; + + return mktime($hrs, $min, $sec, $mon, $day, $year + 1980); + } + + /** + * Returns a date/time string with the specified timestamp format + * + * $time = Date::formatted_time('5 minutes ago'); + * + * @see http://php.net/manual/en/datetime.construct.php + * @param string datetime_str datetime string + * @param string timestamp_format timestamp format + * @return string + */ + public static function formatted_time($datetime_str = 'now', $timestamp_format = NULL, $timezone = NULL) + { + $timestamp_format = $timestamp_format == NULL ? Date::$timestamp_format : $timestamp_format; + $timezone = $timezone === NULL ? Date::$timezone : $timezone; + + $time = new DateTime($datetime_str, new DateTimeZone( + $timezone ? $timezone : date_default_timezone_get() + )); + + return $time->format($timestamp_format); + } + +} // End date diff --git a/includes/kohana/system/classes/kohana/encrypt.php b/includes/kohana/system/classes/kohana/encrypt.php new file mode 100644 index 00000000..7c97389f --- /dev/null +++ b/includes/kohana/system/classes/kohana/encrypt.php @@ -0,0 +1,211 @@ +$name; + + if ( ! isset($config['key'])) + { + // No default encryption key is provided! + throw new Kohana_Exception('No encryption key is defined in the encryption configuration group: :group', + array(':group' => $name)); + } + + if ( ! isset($config['mode'])) + { + // Add the default mode + $config['mode'] = MCRYPT_MODE_NOFB; + } + + if ( ! isset($config['cipher'])) + { + // Add the default cipher + $config['cipher'] = MCRYPT_RIJNDAEL_128; + } + + // Create a new instance + Encrypt::$instances[$name] = new Encrypt($config['key'], $config['mode'], $config['cipher']); + } + + return Encrypt::$instances[$name]; + } + + /** + * Creates a new mcrypt wrapper. + * + * @param string encryption key + * @param string mcrypt mode + * @param string mcrypt cipher + */ + public function __construct($key, $mode, $cipher) + { + // Find the max length of the key, based on cipher and mode + $size = mcrypt_get_key_size($cipher, $mode); + + if (isset($key[$size])) + { + // Shorten the key to the maximum size + $key = substr($key, 0, $size); + } + + // Store the key, mode, and cipher + $this->_key = $key; + $this->_mode = $mode; + $this->_cipher = $cipher; + + // Store the IV size + $this->_iv_size = mcrypt_get_iv_size($this->_cipher, $this->_mode); + } + + /** + * Encrypts a string and returns an encrypted string that can be decoded. + * + * $data = $encrypt->encode($data); + * + * The encrypted binary data is encoded using [base64](http://php.net/base64_encode) + * to convert it to a string. This string can be stored in a database, + * displayed, and passed using most other means without corruption. + * + * @param string data to be encrypted + * @return string + */ + public function encode($data) + { + // Set the rand type if it has not already been set + if (Encrypt::$_rand === NULL) + { + if (Kohana::$is_windows) + { + // Windows only supports the system random number generator + Encrypt::$_rand = MCRYPT_RAND; + } + else + { + if (defined('MCRYPT_DEV_URANDOM')) + { + // Use /dev/urandom + Encrypt::$_rand = MCRYPT_DEV_URANDOM; + } + elseif (defined('MCRYPT_DEV_RANDOM')) + { + // Use /dev/random + Encrypt::$_rand = MCRYPT_DEV_RANDOM; + } + else + { + // Use the system random number generator + Encrypt::$_rand = MCRYPT_RAND; + } + } + } + + if (Encrypt::$_rand === MCRYPT_RAND) + { + // The system random number generator must always be seeded each + // time it is used, or it will not produce true random results + mt_srand(); + } + + // Create a random initialization vector of the proper size for the current cipher + $iv = mcrypt_create_iv($this->_iv_size, Encrypt::$_rand); + + // Encrypt the data using the configured options and generated iv + $data = mcrypt_encrypt($this->_cipher, $this->_key, $data, $this->_mode, $iv); + + // Use base64 encoding to convert to a string + return base64_encode($iv.$data); + } + + /** + * Decrypts an encoded string back to its original value. + * + * $data = $encrypt->decode($data); + * + * @param string encoded string to be decrypted + * @return FALSE if decryption fails + * @return string + */ + public function decode($data) + { + // Convert the data back to binary + $data = base64_decode($data, TRUE); + + if ( ! $data) + { + // Invalid base64 data + return FALSE; + } + + // Extract the initialization vector from the data + $iv = substr($data, 0, $this->_iv_size); + + if ($this->_iv_size !== strlen($iv)) + { + // The iv is not the expected size + return FALSE; + } + + // Remove the iv from the data + $data = substr($data, $this->_iv_size); + + // Return the decrypted data, trimming the \0 padding bytes from the end of the data + return rtrim(mcrypt_decrypt($this->_cipher, $this->_key, $data, $this->_mode, $iv), "\0"); + } + +} // End Encrypt diff --git a/includes/kohana/system/classes/kohana/exception.php b/includes/kohana/system/classes/kohana/exception.php new file mode 100644 index 00000000..60a57f27 --- /dev/null +++ b/includes/kohana/system/classes/kohana/exception.php @@ -0,0 +1,46 @@ + $user)); + * + * @param string error message + * @param array translation variables + * @param integer the exception code + * @return void + */ + public function __construct($message, array $variables = NULL, $code = 0) + { + // Set the message + $message = __($message, $variables); + + // Pass the message to the parent + parent::__construct($message, $code); + } + + /** + * Magic object-to-string method. + * + * echo $exception; + * + * @uses Kohana::exception_text + * @return string + */ + public function __toString() + { + return Kohana::exception_text($this); + } + +} // End Kohana_Exception diff --git a/includes/kohana/system/classes/kohana/feed.php b/includes/kohana/system/classes/kohana/feed.php new file mode 100644 index 00000000..e12fd763 --- /dev/null +++ b/includes/kohana/system/classes/kohana/feed.php @@ -0,0 +1,176 @@ +getNamespaces(true); + + // Detect the feed type. RSS 1.0/2.0 and Atom 1.0 are supported. + $feed = isset($feed->channel) ? $feed->xpath('//item') : $feed->entry; + + $i = 0; + $items = array(); + + foreach ($feed as $item) + { + if ($limit > 0 AND $i++ === $limit) + break; + $item_fields = (array) $item; + + // get namespaced tags + foreach ($namespaces as $ns) + { + $item_fields += (array) $item->children($ns); + } + $items[] = $item_fields; + } + + return $items; + } + + /** + * Creates a feed from the given parameters. + * + * @param array feed information + * @param array items to add to the feed + * @param string define which format to use (only rss2 is supported) + * @param string define which encoding to use + * @return string + */ + public static function create($info, $items, $format = 'rss2', $encoding = 'UTF-8') + { + $info += array('title' => 'Generated Feed', 'link' => '', 'generator' => 'KohanaPHP'); + + $feed = ''; + $feed = simplexml_load_string($feed); + + foreach ($info as $name => $value) + { + if ($name === 'image') + { + // Create an image element + $image = $feed->channel->addChild('image'); + + if ( ! isset($value['link'], $value['url'], $value['title'])) + { + throw new Kohana_Exception('Feed images require a link, url, and title'); + } + + if (strpos($value['link'], '://') === FALSE) + { + // Convert URIs to URLs + $value['link'] = URL::site($value['link'], 'http'); + } + + if (strpos($value['url'], '://') === FALSE) + { + // Convert URIs to URLs + $value['url'] = URL::site($value['url'], 'http'); + } + + // Create the image elements + $image->addChild('link', $value['link']); + $image->addChild('url', $value['url']); + $image->addChild('title', $value['title']); + } + else + { + if (($name === 'pubDate' OR $name === 'lastBuildDate') AND (is_int($value) OR ctype_digit($value))) + { + // Convert timestamps to RFC 822 formatted dates + $value = date('r', $value); + } + elseif (($name === 'link' OR $name === 'docs') AND strpos($value, '://') === FALSE) + { + // Convert URIs to URLs + $value = URL::site($value, 'http'); + } + + // Add the info to the channel + $feed->channel->addChild($name, $value); + } + } + + foreach ($items as $item) + { + // Add the item to the channel + $row = $feed->channel->addChild('item'); + + foreach ($item as $name => $value) + { + if ($name === 'pubDate' AND (is_int($value) OR ctype_digit($value))) + { + // Convert timestamps to RFC 822 formatted dates + $value = date('r', $value); + } + elseif (($name === 'link' OR $name === 'guid') AND strpos($value, '://') === FALSE) + { + // Convert URIs to URLs + $value = URL::site($value, 'http'); + } + + // Add the info to the row + $row->addChild($name, $value); + } + } + + if (function_exists('dom_import_simplexml')) + { + // Convert the feed object to a DOM object + $feed = dom_import_simplexml($feed)->ownerDocument; + + // DOM generates more readable XML + $feed->formatOutput = TRUE; + + // Export the document as XML + $feed = $feed->saveXML(); + } + else + { + // Export the document as XML + $feed = $feed->asXML(); + } + + return $feed; + } + +} // End Feed diff --git a/includes/kohana/system/classes/kohana/file.php b/includes/kohana/system/classes/kohana/file.php new file mode 100644 index 00000000..a53def60 --- /dev/null +++ b/includes/kohana/system/classes/kohana/file.php @@ -0,0 +1,179 @@ +file($filename); + } + } + + if (ini_get('mime_magic.magicfile') AND function_exists('mime_content_type')) + { + // The mime_content_type function is only useful with a magic file + return mime_content_type($filename); + } + + if ( ! empty($extension)) + { + return File::mime_by_ext($extension); + } + + // Unable to find the mime-type + return FALSE; + } + + /** + * Return the mime type of an extension. + * + * $mime = File::mime_by_ext('png'); // "image/png" + * + * @param string extension: php, pdf, txt, etc + * @return string mime type on success + * @return FALSE on failure + */ + public static function mime_by_ext($extension) + { + // Load all of the mime types + $mimes = Kohana::config('mimes'); + + return isset($mimes[$extension]) ? $mimes[$extension][0] : FALSE; + } + + /** + * Split a file into pieces matching a specific size. Used when you need to + * split large files into smaller pieces for easy transmission. + * + * $count = File::split($file); + * + * @param string file to be split + * @param string directory to output to, defaults to the same directory as the file + * @param integer size, in MB, for each piece to be + * @return integer The number of pieces that were created + */ + public static function split($filename, $piece_size = 10) + { + // Open the input file + $file = fopen($filename, 'rb'); + + // Change the piece size to bytes + $piece_size = floor($piece_size * 1024 * 1024); + + // Write files in 8k blocks + $block_size = 1024 * 8; + + // Total number of peices + $peices = 0; + + while ( ! feof($file)) + { + // Create another piece + $peices += 1; + + // Create a new file piece + $piece = str_pad($peices, 3, '0', STR_PAD_LEFT); + $piece = fopen($filename.'.'.$piece, 'wb+'); + + // Number of bytes read + $read = 0; + + do + { + // Transfer the data in blocks + fwrite($piece, fread($file, $block_size)); + + // Another block has been read + $read += $block_size; + } + while ($read < $piece_size); + + // Close the piece + fclose($piece); + } + + // Close the file + fclose($file); + + return $peices; + } + + /** + * Join a split file into a whole file. Does the reverse of [File::split]. + * + * $count = File::join($file); + * + * @param string split filename, without .000 extension + * @param string output filename, if different then an the filename + * @return integer The number of pieces that were joined. + */ + public static function join($filename) + { + // Open the file + $file = fopen($filename, 'wb+'); + + // Read files in 8k blocks + $block_size = 1024 * 8; + + // Total number of peices + $pieces = 0; + + while (is_file($piece = $filename.'.'.str_pad($pieces + 1, 3, '0', STR_PAD_LEFT))) + { + // Read another piece + $pieces += 1; + + // Open the piece for reading + $piece = fopen($piece, 'rb'); + + while ( ! feof($piece)) + { + // Transfer the data in blocks + fwrite($file, fread($piece, $block_size)); + } + + // Close the peice + fclose($piece); + } + + return $pieces; + } + +} // End file diff --git a/includes/kohana/system/classes/kohana/form.php b/includes/kohana/system/classes/kohana/form.php new file mode 100644 index 00000000..e1e7e38a --- /dev/null +++ b/includes/kohana/system/classes/kohana/form.php @@ -0,0 +1,434 @@ + 'get')); + * + * // When "file" inputs are present, you must include the "enctype" + * echo Form::open(NULL, array('enctype' => 'multipart/form-data')); + * + * @param string form action, defaults to the current request URI + * @param array html attributes + * @return string + * @uses Request::instance + * @uses URL::site + * @uses HTML::attributes + */ + public static function open($action = NULL, array $attributes = NULL) + { + if ($action === NULL) + { + // Use the current URI + $action = Request::current()->uri; + } + + if ($action === '') + { + // Use only the base URI + $action = Kohana::$base_url; + } + elseif (strpos($action, '://') === FALSE) + { + // Make the URI absolute + $action = URL::site($action); + } + + // Add the form action to the attributes + $attributes['action'] = $action; + + // Only accept the default character set + $attributes['accept-charset'] = Kohana::$charset; + + if ( ! isset($attributes['method'])) + { + // Use POST method + $attributes['method'] = 'post'; + } + + return ''; + } + + /** + * Creates the closing form tag. + * + * echo Form::close(); + * + * @return string + */ + public static function close() + { + return ''; + } + + /** + * Creates a form input. If no type is specified, a "text" type input will + * be returned. + * + * echo Form::input('username', $username); + * + * @param string input name + * @param string input value + * @param array html attributes + * @return string + * @uses HTML::attributes + */ + public static function input($name, $value = NULL, array $attributes = NULL) + { + // Set the input name + $attributes['name'] = $name; + + // Set the input value + $attributes['value'] = $value; + + if ( ! isset($attributes['type'])) + { + // Default type is text + $attributes['type'] = 'text'; + } + + return ''; + } + + /** + * Creates a hidden form input. + * + * echo Form::hidden('csrf', $token); + * + * @param string input name + * @param string input value + * @param array html attributes + * @return string + * @uses Form::input + */ + public static function hidden($name, $value = NULL, array $attributes = NULL) + { + $attributes['type'] = 'hidden'; + + return Form::input($name, $value, $attributes); + } + + /** + * Creates a password form input. + * + * echo Form::password('password'); + * + * @param string input name + * @param string input value + * @param array html attributes + * @return string + * @uses Form::input + */ + public static function password($name, $value = NULL, array $attributes = NULL) + { + $attributes['type'] = 'password'; + + return Form::input($name, $value, $attributes); + } + + /** + * Creates a file upload form input. No input value can be specified. + * + * echo Form::file('image'); + * + * @param string input name + * @param array html attributes + * @return string + * @uses Form::input + */ + public static function file($name, array $attributes = NULL) + { + $attributes['type'] = 'file'; + + return Form::input($name, NULL, $attributes); + } + + /** + * Creates a checkbox form input. + * + * echo Form::checkbox('remember_me', 1, (bool) $remember); + * + * @param string input name + * @param string input value + * @param boolean checked status + * @param array html attributes + * @return string + * @uses Form::input + */ + public static function checkbox($name, $value = NULL, $checked = FALSE, array $attributes = NULL) + { + $attributes['type'] = 'checkbox'; + + if ($checked === TRUE) + { + // Make the checkbox active + $attributes['checked'] = 'checked'; + } + + return Form::input($name, $value, $attributes); + } + + /** + * Creates a radio form input. + * + * echo Form::radio('like_cats', 1, $cats); + * echo Form::radio('like_cats', 0, ! $cats); + * + * @param string input name + * @param string input value + * @param boolean checked status + * @param array html attributes + * @return string + * @uses Form::input + */ + public static function radio($name, $value = NULL, $checked = FALSE, array $attributes = NULL) + { + $attributes['type'] = 'radio'; + + if ($checked === TRUE) + { + // Make the radio active + $attributes['checked'] = 'checked'; + } + + return Form::input($name, $value, $attributes); + } + + /** + * Creates a textarea form input. + * + * echo Form::textarea('about', $about); + * + * @param string textarea name + * @param string textarea body + * @param array html attributes + * @param boolean encode existing HTML characters + * @return string + * @uses HTML::attributes + * @uses HTML::chars + */ + public static function textarea($name, $body = '', array $attributes = NULL, $double_encode = TRUE) + { + // Set the input name + $attributes['name'] = $name; + + // Add default rows and cols attributes (required) + $attributes += array('rows' => 10, 'cols' => 50); + + return ''.HTML::chars($body, $double_encode).''; + } + + /** + * Creates a select form input. + * + * echo Form::select('country', $countries, $country); + * + * [!!] Support for multiple selected options was added in v3.0.7. + * + * @param string input name + * @param array available options + * @param mixed selected option string, or an array of selected options + * @param array html attributes + * @return string + * @uses HTML::attributes + */ + public static function select($name, array $options = NULL, $selected = NULL, array $attributes = NULL) + { + // Set the input name + $attributes['name'] = $name; + + if (is_array($selected)) + { + // This is a multi-select, god save us! + $attributes['multiple'] = 'multiple'; + } + + if ( ! is_array($selected)) + { + if ($selected === NULL) + { + // Use an empty array + $selected = array(); + } + else + { + // Convert the selected options to an array + $selected = array((string) $selected); + } + } + + if (empty($options)) + { + // There are no options + $options = ''; + } + else + { + foreach ($options as $value => $name) + { + if (is_array($name)) + { + // Create a new optgroup + $group = array('label' => $value); + + // Create a new list of options + $_options = array(); + + foreach ($name as $_value => $_name) + { + // Force value to be string + $_value = (string) $_value; + + // Create a new attribute set for this option + $option = array('value' => $_value); + + if (in_array($_value, $selected)) + { + // This option is selected + $option['selected'] = 'selected'; + } + + // Change the option to the HTML string + $_options[] = ''.HTML::chars($_name, FALSE).''; + } + + // Compile the options into a string + $_options = "\n".implode("\n", $_options)."\n"; + + $options[$value] = ''.$_options.''; + } + else + { + // Force value to be string + $value = (string) $value; + + // Create a new attribute set for this option + $option = array('value' => $value); + + if (in_array($value, $selected)) + { + // This option is selected + $option['selected'] = 'selected'; + } + + // Change the option to the HTML string + $options[$value] = ''.HTML::chars($name, FALSE).''; + } + } + + // Compile the options into a single string + $options = "\n".implode("\n", $options)."\n"; + } + + return ''.$options.''; + } + + /** + * Creates a submit form input. + * + * echo Form::submit(NULL, 'Login'); + * + * @param string input name + * @param string input value + * @param array html attributes + * @return string + * @uses Form::input + */ + public static function submit($name, $value, array $attributes = NULL) + { + $attributes['type'] = 'submit'; + + return Form::input($name, $value, $attributes); + } + + /** + * Creates a image form input. + * + * echo Form::image(NULL, NULL, array('src' => 'media/img/login.png')); + * + * @param string input name + * @param string input value + * @param array html attributes + * @param boolean add index file to URL? + * @return string + * @uses Form::input + */ + public static function image($name, $value, array $attributes = NULL, $index = FALSE) + { + if ( ! empty($attributes['src'])) + { + if (strpos($attributes['src'], '://') === FALSE) + { + // Add the base URL + $attributes['src'] = URL::base($index).$attributes['src']; + } + } + + $attributes['type'] = 'image'; + + return Form::input($name, $value, $attributes); + } + + /** + * Creates a button form input. Note that the body of a button is NOT escaped, + * to allow images and other HTML to be used. + * + * echo Form::button('save', 'Save Profile', array('type' => 'submit')); + * + * @param string input name + * @param string input value + * @param array html attributes + * @return string + * @uses HTML::attributes + */ + public static function button($name, $body, array $attributes = NULL) + { + // Set the input name + $attributes['name'] = $name; + + return ''.$body.''; + } + + /** + * Creates a form label. Label text is not automatically translated. + * + * echo Form::label('username', 'Username'); + * + * @param string target input + * @param string label text + * @param array html attributes + * @return string + * @uses HTML::attributes + */ + public static function label($input, $text = NULL, array $attributes = NULL) + { + if ($text === NULL) + { + // Use the input name as the text + $text = ucwords(preg_replace('/[\W_]+/', ' ', $input)); + } + + // Set the label target + $attributes['for'] = $input; + + return ''.$text.''; + } + +} // End form diff --git a/includes/kohana/system/classes/kohana/fragment.php b/includes/kohana/system/classes/kohana/fragment.php new file mode 100644 index 00000000..ea528179 --- /dev/null +++ b/includes/kohana/system/classes/kohana/fragment.php @@ -0,0 +1,145 @@ + cache key + protected static $_caches = array(); + + /** + * Generate the cache key name for a fragment. + * + * $key = Fragment::_cache_key('footer', TRUE); + * + * @param string fragment name + * @param boolean multilingual fragment support + * @return string + * @uses I18n::lang + * @since 3.0.4 + */ + protected static function _cache_key($name, $i18n = NULL) + { + if ($i18n === NULL) + { + // Use the default setting + $i18n = Fragment::$i18n; + } + + // Language prefix for cache key + $i18n = ($i18n === TRUE) ? I18n::lang() : ''; + + // Note: $i18n and $name need to be delimited to prevent naming collisions + return 'Fragment::cache('.$i18n.'+'.$name.')'; + } + + /** + * Load a fragment from cache and display it. Multiple fragments can + * be nested with different life times. + * + * if ( ! Fragment::load('footer')) { + * // Anything that is echo'ed here will be saved + * Fragment::save(); + * } + * + * @param string fragment name + * @param integer fragment cache lifetime + * @param boolean multilingual fragment support + * @return boolean + */ + public static function load($name, $lifetime = NULL, $i18n = NULL) + { + // Set the cache lifetime + $lifetime = ($lifetime === NULL) ? Fragment::$lifetime : (int) $lifetime; + + // Get the cache key name + $cache_key = Fragment::_cache_key($name, $i18n); + + if ($fragment = Kohana::cache($cache_key, NULL, $lifetime)) + { + // Display the cached fragment now + echo $fragment; + + return TRUE; + } + else + { + // Start the output buffer + ob_start(); + + // Store the cache key by the buffer level + Fragment::$_caches[ob_get_level()] = $cache_key; + + return FALSE; + } + } + + /** + * Saves the currently open fragment in the cache. + * + * Fragment::save(); + * + * @return void + */ + public static function save() + { + // Get the buffer level + $level = ob_get_level(); + + if (isset(Fragment::$_caches[$level])) + { + // Get the cache key based on the level + $cache_key = Fragment::$_caches[$level]; + + // Delete the cache key, we don't need it anymore + unset(Fragment::$_caches[$level]); + + // Get the output buffer and display it at the same time + $fragment = ob_get_flush(); + + // Cache the fragment + Kohana::cache($cache_key, $fragment); + } + } + + /** + * Delete a cached fragment. + * + * Fragment::delete($key); + * + * @param string fragment name + * @param boolean multilingual fragment support + * @return void + */ + public static function delete($name, $i18n = NULL) + { + // Invalid the cache + Kohana::cache(Fragment::_cache_key($name, $i18n), NULL, -3600); + } + +} // End Fragment diff --git a/includes/kohana/system/classes/kohana/html.php b/includes/kohana/system/classes/kohana/html.php new file mode 100644 index 00000000..17f27ce8 --- /dev/null +++ b/includes/kohana/system/classes/kohana/html.php @@ -0,0 +1,367 @@ +'.$title.''; + } + + /** + * Creates an HTML anchor to a file. Note that the title is not escaped, + * to allow HTML elements within links (images, etc). + * + * echo HTML::file_anchor('media/doc/user_guide.pdf', 'User Guide'); + * + * @param string name of file to link to + * @param string link text + * @param array HTML anchor attributes + * @param string non-default protocol, eg: ftp + * @return string + * @uses URL::base + * @uses HTML::attributes + */ + public static function file_anchor($file, $title = NULL, array $attributes = NULL, $protocol = NULL) + { + if ($title === NULL) + { + // Use the file name as the title + $title = basename($file); + } + + // Add the file link to the attributes + $attributes['href'] = URL::base(FALSE, $protocol).$file; + + return ''.$title.''; + } + + /** + * Generates an obfuscated version of a string. Text passed through this + * method is less likely to be read by web crawlers and robots, which can + * be helpful for spam prevention, but can prevent legitimate robots from + * reading your content. + * + * echo HTML::obfuscate($text); + * + * @param string string to obfuscate + * @return string + * @since 3.0.3 + */ + public static function obfuscate($string) + { + $safe = ''; + foreach (str_split($string) as $letter) + { + switch (rand(1, 3)) + { + // HTML entity code + case 1: $safe .= '&#'.ord($letter).';'; break; + // Hex character code + case 2: $safe .= '&#x'.dechex(ord($letter)).';'; break; + // Raw (no) encoding + case 3: $safe .= $letter; + } + } + + return $safe; + } + + /** + * Generates an obfuscated version of an email address. Helps prevent spam + * robots from finding email addresses. + * + * echo HTML::email($address); + * + * @param string email address + * @return string + * @uses HTML::obfuscate + */ + public static function email($email) + { + // Make sure the at sign is always obfuscated + return str_replace('@', '@', HTML::obfuscate($email)); + } + + /** + * Creates an email (mailto:) anchor. Note that the title is not escaped, + * to allow HTML elements within links (images, etc). + * + * echo HTML::mailto($address); + * + * @param string email address to send to + * @param string link text + * @param array HTML anchor attributes + * @return string + * @uses HTML::email + * @uses HTML::attributes + */ + public static function mailto($email, $title = NULL, array $attributes = NULL) + { + // Obfuscate email address + $email = HTML::email($email); + + if ($title === NULL) + { + // Use the email address as the title + $title = $email; + } + + return ''.$title.''; + } + + /** + * Creates a style sheet link element. + * + * echo HTML::style('media/css/screen.css'); + * + * @param string file name + * @param array default attributes + * @param boolean include the index page + * @return string + * @uses URL::base + * @uses HTML::attributes + */ + public static function style($file, array $attributes = NULL, $index = FALSE) + { + if (strpos($file, '://') === FALSE) + { + // Add the base URL + $file = URL::base($index).$file; + } + + // Set the stylesheet link + $attributes['href'] = $file; + + // Set the stylesheet rel + $attributes['rel'] = 'stylesheet'; + + // Set the stylesheet type + $attributes['type'] = 'text/css'; + + return ''; + } + + /** + * Creates a script link. + * + * echo HTML::script('media/js/jquery.min.js'); + * + * @param string file name + * @param array default attributes + * @param boolean include the index page + * @return string + * @uses URL::base + * @uses HTML::attributes + */ + public static function script($file, array $attributes = NULL, $index = FALSE) + { + if (strpos($file, '://') === FALSE) + { + // Add the base URL + $file = URL::base($index).$file; + } + + // Set the script link + $attributes['src'] = $file; + + // Set the script type + $attributes['type'] = 'text/javascript'; + + return ''; + } + + /** + * Creates a image link. + * + * echo HTML::image('media/img/logo.png', array('alt' => 'My Company')); + * + * @param string file name + * @param array default attributes + * @return string + * @uses URL::base + * @uses HTML::attributes + */ + public static function image($file, array $attributes = NULL, $index = FALSE) + { + if (strpos($file, '://') === FALSE) + { + // Add the base URL + $file = URL::base($index).$file; + } + + // Add the image link + $attributes['src'] = $file; + + return ''; + } + + /** + * Compiles an array of HTML attributes into an attribute string. + * Attributes will be sorted using HTML::$attribute_order for consistency. + * + * echo ''.$content.'
            '; + * + * @param array attribute list + * @return string + */ + public static function attributes(array $attributes = NULL) + { + if (empty($attributes)) + return ''; + + $sorted = array(); + foreach (HTML::$attribute_order as $key) + { + if (isset($attributes[$key])) + { + // Add the attribute to the sorted list + $sorted[$key] = $attributes[$key]; + } + } + + // Combine the sorted attributes + $attributes = $sorted + $attributes; + + $compiled = ''; + foreach ($attributes as $key => $value) + { + if ($value === NULL) + { + // Skip attributes that have NULL values + continue; + } + + // Add the attribute value + $compiled .= ' '.$key.'="'.HTML::chars($value).'"'; + } + + return $compiled; + } + +} // End html diff --git a/includes/kohana/system/classes/kohana/i18n.php b/includes/kohana/system/classes/kohana/i18n.php new file mode 100644 index 00000000..433d465c --- /dev/null +++ b/includes/kohana/system/classes/kohana/i18n.php @@ -0,0 +1,132 @@ + $username)); + * + * [!!] The __() function is declared in `SYSPATH/base.php`. + * + * @package Kohana + * @category Base + * @author Kohana Team + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_I18n { + + /** + * @var string target language: en-us, es-es, zh-cn, etc + */ + public static $lang = 'en-us'; + + // Cache of loaded languages + protected static $_cache = array(); + + /** + * Get and set the target language. + * + * // Get the current language + * $lang = I18n::lang(); + * + * // Change the current language to Spanish + * I18n::lang('es-es'); + * + * @param string new language setting + * @return string + * @since 3.0.2 + */ + public static function lang($lang = NULL) + { + if ($lang) + { + // Normalize the language + I18n::$lang = strtolower(str_replace(array(' ', '_'), '-', $lang)); + } + + return I18n::$lang; + } + + /** + * Returns translation of a string. If no translation exists, the original + * string will be returned. No parameters are replaced. + * + * $hello = I18n::get('Hello friends, my name is :name'); + * + * @param string text to translate + * @param string target language + * @return string + */ + public static function get($string, $lang = NULL) + { + if ( ! $lang) + { + // Use the global target language + $lang = I18n::$lang; + } + + // Load the translation table for this language + $table = I18n::load($lang); + + // Return the translated string if it exists + return isset($table[$string]) ? $table[$string] : $string; + } + + /** + * Returns the translation table for a given language. + * + * // Get all defined Spanish messages + * $messages = I18n::load('es-es'); + * + * @param string language to load + * @return array + */ + public static function load($lang) + { + if (isset(I18n::$_cache[$lang])) + { + return I18n::$_cache[$lang]; + } + + // New translation table + $table = array(); + + // Split the language: language, region, locale, etc + $parts = explode('-', $lang); + + do + { + // Create a path for this set of parts + $path = implode(DIRECTORY_SEPARATOR, $parts); + + if ($files = Kohana::find_file('i18n', $path, NULL, TRUE)) + { + $t = array(); + foreach ($files as $file) + { + // Merge the language strings into the sub table + $t = array_merge($t, Kohana::load($file)); + } + + // Append the sub table, preventing less specific language + // files from overloading more specific files + $table += $t; + } + + // Remove the last part + array_pop($parts); + } + while ($parts); + + // Cache the translation table locally + return I18n::$_cache[$lang] = $table; + } + +} // End I18n diff --git a/includes/kohana/system/classes/kohana/inflector.php b/includes/kohana/system/classes/kohana/inflector.php new file mode 100644 index 00000000..3d3d5fbb --- /dev/null +++ b/includes/kohana/system/classes/kohana/inflector.php @@ -0,0 +1,237 @@ +uncountable; + + // Make uncountables mirrored + Inflector::$uncountable = array_combine(Inflector::$uncountable, Inflector::$uncountable); + } + + return isset(Inflector::$uncountable[strtolower($str)]); + } + + /** + * Makes a plural word singular. + * + * echo Inflector::singular('cats'); // "cat" + * echo Inflector::singular('fish'); // "fish", uncountable + * + * You can also provide the count to make inflection more intelligent. + * In this case, it will only return the singular value if the count is + * greater than one and not zero. + * + * echo Inflector::singular('cats', 2); // "cats" + * + * [!!] Special inflections are defined in `config/inflector.php`. + * + * @param string word to singularize + * @param integer count of thing + * @return string + * @uses Inflector::uncountable + */ + public static function singular($str, $count = NULL) + { + // $count should always be a float + $count = ($count === NULL) ? 1.0 : (float) $count; + + // Do nothing when $count is not 1 + if ($count != 1) + return $str; + + // Remove garbage + $str = strtolower(trim($str)); + + // Cache key name + $key = 'singular_'.$str.$count; + + if (isset(Inflector::$cache[$key])) + return Inflector::$cache[$key]; + + if (Inflector::uncountable($str)) + return Inflector::$cache[$key] = $str; + + if (empty(Inflector::$irregular)) + { + // Cache irregular words + Inflector::$irregular = Kohana::config('inflector')->irregular; + } + + if ($irregular = array_search($str, Inflector::$irregular)) + { + $str = $irregular; + } + elseif (preg_match('/us$/', $str)) + { + // http://en.wikipedia.org/wiki/Plural_form_of_words_ending_in_-us + // Already singular, do nothing + } + elseif (preg_match('/[sxz]es$/', $str) OR preg_match('/[^aeioudgkprt]hes$/', $str)) + { + // Remove "es" + $str = substr($str, 0, -2); + } + elseif (preg_match('/[^aeiou]ies$/', $str)) + { + // Replace "ies" with "y" + $str = substr($str, 0, -3).'y'; + } + elseif (substr($str, -1) === 's' AND substr($str, -2) !== 'ss') + { + // Remove singular "s" + $str = substr($str, 0, -1); + } + + return Inflector::$cache[$key] = $str; + } + + /** + * Makes a singular word plural. + * + * echo Inflector::plural('fish'); // "fish", uncountable + * echo Inflector::plural('cat'); // "cats" + * + * You can also provide the count to make inflection more intelligent. + * In this case, it will only return the plural value if the count is + * not one. + * + * echo Inflector::singular('cats', 3); // "cats" + * + * [!!] Special inflections are defined in `config/inflector.php`. + * + * @param string word to pluralize + * @param integer count of thing + * @return string + * @uses Inflector::uncountable + */ + public static function plural($str, $count = NULL) + { + // $count should always be a float + $count = ($count === NULL) ? 0.0 : (float) $count; + + // Do nothing with singular + if ($count == 1) + return $str; + + // Remove garbage + $str = strtolower(trim($str)); + + // Cache key name + $key = 'plural_'.$str.$count; + + if (isset(Inflector::$cache[$key])) + return Inflector::$cache[$key]; + + if (Inflector::uncountable($str)) + return Inflector::$cache[$key] = $str; + + if (empty(Inflector::$irregular)) + { + // Cache irregular words + Inflector::$irregular = Kohana::config('inflector')->irregular; + } + + if (isset(Inflector::$irregular[$str])) + { + $str = Inflector::$irregular[$str]; + } + elseif (preg_match('/[sxz]$/', $str) OR preg_match('/[^aeioudgkprt]h$/', $str)) + { + $str .= 'es'; + } + elseif (preg_match('/[^aeiou]y$/', $str)) + { + // Change "y" to "ies" + $str = substr_replace($str, 'ies', -1); + } + else + { + $str .= 's'; + } + + // Set the cache and return + return Inflector::$cache[$key] = $str; + } + + /** + * Makes a phrase camel case. Spaces and underscores will be removed. + * + * $str = Inflector::camelize('mother cat'); // "motherCat" + * $str = Inflector::camelize('kittens in bed'); // "kittensInBed" + * + * @param string phrase to camelize + * @return string + */ + public static function camelize($str) + { + $str = 'x'.strtolower(trim($str)); + $str = ucwords(preg_replace('/[\s_]+/', ' ', $str)); + + return substr(str_replace(' ', '', $str), 1); + } + + /** + * Makes a phrase underscored instead of spaced. + * + * $str = Inflector::underscore('five cats'); // "five_cats"; + * + * @param string phrase to underscore + * @return string + */ + public static function underscore($str) + { + return preg_replace('/\s+/', '_', trim($str)); + } + + /** + * Makes an underscored or dashed phrase human-readable. + * + * $str = Inflector::humanize('kittens-are-cats'); // "kittens are cats" + * $str = Inflector::humanize('dogs_as_well'); // "dogs as well" + * + * @param string phrase to make human-readable + * @return string + */ + public static function humanize($str) + { + return preg_replace('/[_-]+/', ' ', trim($str)); + } + +} // End Inflector diff --git a/includes/kohana/system/classes/kohana/log.php b/includes/kohana/system/classes/kohana/log.php new file mode 100644 index 00000000..156ffe78 --- /dev/null +++ b/includes/kohana/system/classes/kohana/log.php @@ -0,0 +1,185 @@ +attach($writer); + * + * @param object Kohana_Log_Writer instance + * @param array messages types to write + * @return $this + */ + public function attach(Kohana_Log_Writer $writer, array $types = NULL) + { + $this->_writers["{$writer}"] = array + ( + 'object' => $writer, + 'types' => $types + ); + + return $this; + } + + /** + * Detaches a log writer. The same writer object must be used. + * + * $log->detach($writer); + * + * @param object Kohana_Log_Writer instance + * @return $this + */ + public function detach(Kohana_Log_Writer $writer) + { + // Remove the writer + unset($this->_writers["{$writer}"]); + + return $this; + } + + /** + * Adds a message to the log. Replacement values must be passed in to be + * replaced using [strtr](http://php.net/strtr). + * + * $log->add('error', 'Could not locate user: :user', array( + * ':user' => $username, + * )); + * + * @param string type of message + * @param string message body + * @param array values to replace in the message + * @return $this + */ + public function add($type, $message, array $values = NULL) + { + if ($values) + { + // Insert the values into the message + $message = strtr($message, $values); + } + + // Create a new message and timestamp it + $this->_messages[] = array + ( + 'time' => Date::formatted_time('now', self::$timestamp, self::$timezone), + 'type' => $type, + 'body' => $message, + ); + + if (self::$write_on_add) + { + // Write logs as they are added + $this->write(); + } + + return $this; + } + + /** + * Write and clear all of the messages. + * + * $log->write(); + * + * @return void + */ + public function write() + { + if (empty($this->_messages)) + { + // There is nothing to write, move along + return; + } + + // Import all messages locally + $messages = $this->_messages; + + // Reset the messages array + $this->_messages = array(); + + foreach ($this->_writers as $writer) + { + if (empty($writer['types'])) + { + // Write all of the messages + $writer['object']->write($messages); + } + else + { + // Filtered messages + $filtered = array(); + + foreach ($messages as $message) + { + if (in_array($message['type'], $writer['types'])) + { + // Writer accepts this kind of message + $filtered[] = $message; + } + } + + // Write the filtered messages + $writer['object']->write($filtered); + } + } + } + +} // End Kohana_Log diff --git a/includes/kohana/system/classes/kohana/log/file.php b/includes/kohana/system/classes/kohana/log/file.php new file mode 100644 index 00000000..66a7809d --- /dev/null +++ b/includes/kohana/system/classes/kohana/log/file.php @@ -0,0 +1,95 @@ + Kohana::debug_path($directory))); + } + + // Determine the directory path + $this->_directory = realpath($directory).DIRECTORY_SEPARATOR; + } + + /** + * Writes each of the messages into the log file. The log file will be + * appended to the `YYYY/MM/DD.log.php` file, where YYYY is the current + * year, MM is the current month, and DD is the current day. + * + * $writer->write($messages); + * + * @param array messages + * @return void + */ + public function write(array $messages) + { + // Set the yearly directory name + $directory = $this->_directory.date('Y').DIRECTORY_SEPARATOR; + + if ( ! is_dir($directory)) + { + // Create the yearly directory + mkdir($directory, 0777); + + // Set permissions (must be manually set to fix umask issues) + chmod($directory, 0777); + } + + // Add the month to the directory + $directory .= date('m').DIRECTORY_SEPARATOR; + + if ( ! is_dir($directory)) + { + // Create the yearly directory + mkdir($directory, 0777); + + // Set permissions (must be manually set to fix umask issues) + chmod($directory, 0777); + } + + // Set the name of the log file + $filename = $directory.date('d').EXT; + + if ( ! file_exists($filename)) + { + // Create the log file + file_put_contents($filename, Kohana::FILE_SECURITY.' ?>'.PHP_EOL); + + // Allow anyone to write to log files + chmod($filename, 0666); + } + + // Set the log line format + $format = 'time --- type: body'; + + foreach ($messages as $message) + { + // Write each message into the log file + file_put_contents($filename, PHP_EOL.strtr($format, $message), FILE_APPEND); + } + } + +} // End Kohana_Log_File \ No newline at end of file diff --git a/includes/kohana/system/classes/kohana/log/syslog.php b/includes/kohana/system/classes/kohana/log/syslog.php new file mode 100644 index 00000000..bc5ae578 --- /dev/null +++ b/includes/kohana/system/classes/kohana/log/syslog.php @@ -0,0 +1,65 @@ + LOG_ERR, + 'CRITICAL' => LOG_CRIT, + 'STRACE' => LOG_ALERT, + 'ALERT' => LOG_WARNING, + 'INFO' => LOG_INFO, + 'DEBUG' => LOG_DEBUG); + + /** + * Creates a new syslog logger. + * + * @see http://us2.php.net/openlog + * + * @param string syslog identifier + * @param int facility to log to + * @return void + */ + public function __construct($ident = 'KohanaPHP', $facility = LOG_USER) + { + $this->_ident = $ident; + + // Open the connection to syslog + openlog($this->_ident, LOG_CONS, $facility); + } + + /** + * Writes each of the messages into the syslog. + * + * @param array messages + * @return void + */ + public function write(array $messages) + { + foreach ($messages as $message) + { + syslog($this->_syslog_levels[$message['type']], $message['body']); + } + } + + /** + * Closes the syslog connection + * + * @return void + */ + public function __destruct() + { + // Close connection to syslog + closelog(); + } + +} // End Kohana_Log_Syslog \ No newline at end of file diff --git a/includes/kohana/system/classes/kohana/log/writer.php b/includes/kohana/system/classes/kohana/log/writer.php new file mode 100644 index 00000000..9a09ac63 --- /dev/null +++ b/includes/kohana/system/classes/kohana/log/writer.php @@ -0,0 +1,35 @@ +write($messages); + * + * @param array messages + * @return void + */ + abstract public function write(array $messages); + + /** + * Allows the writer to have a unique key when stored. + * + * echo $writer; + * + * @return string + */ + final public function __toString() + { + return spl_object_hash($this); + } + +} // End Kohana_Log_Writer diff --git a/includes/kohana/system/classes/kohana/model.php b/includes/kohana/system/classes/kohana/model.php new file mode 100644 index 00000000..3aa0ca0d --- /dev/null +++ b/includes/kohana/system/classes/kohana/model.php @@ -0,0 +1,58 @@ +_db = $db; + } + + if (is_string($this->_db)) + { + // Load the database + $this->_db = Database::instance($this->_db); + } + } + +} // End Model diff --git a/includes/kohana/system/classes/kohana/num.php b/includes/kohana/system/classes/kohana/num.php new file mode 100644 index 00000000..20205720 --- /dev/null +++ b/includes/kohana/system/classes/kohana/num.php @@ -0,0 +1,81 @@ + 10 AND $number % 100 < 14) + { + return 'th'; + } + + switch ($number % 10) + { + case 1: + return 'st'; + case 2: + return 'nd'; + case 3: + return 'rd'; + default: + return 'th'; + } + } + + /** + * Locale-aware number and monetary formatting. + * + * // In English, "1,200.05" + * // In Spanish, "1200,05" + * // In Portuguese, "1 200,05" + * echo Num::format(1200.05, 2); + * + * // In English, "1,200.05" + * // In Spanish, "1.200,05" + * // In Portuguese, "1.200.05" + * echo Num::format(1200.05, 2, TRUE); + * + * @param float number to format + * @param integer decimal places + * @param boolean monetary formatting? + * @return string + * @since 3.0.2 + */ + public static function format($number, $places, $monetary = FALSE) + { + $info = localeconv(); + + if ($monetary) + { + $decimal = $info['mon_decimal_point']; + $thousands = $info['mon_thousands_sep']; + } + else + { + $decimal = $info['decimal_point']; + $thousands = $info['thousands_sep']; + } + + return number_format($number, $places, $decimal, $thousands); + } + +} // End num diff --git a/includes/kohana/system/classes/kohana/profiler.php b/includes/kohana/system/classes/kohana/profiler.php new file mode 100644 index 00000000..6d7d9f7c --- /dev/null +++ b/includes/kohana/system/classes/kohana/profiler.php @@ -0,0 +1,375 @@ + strtolower($group), + 'name' => (string) $name, + + // Start the benchmark + 'start_time' => microtime(TRUE), + 'start_memory' => memory_get_usage(), + + // Set the stop keys without values + 'stop_time' => FALSE, + 'stop_memory' => FALSE, + ); + + return $token; + } + + /** + * Stops a benchmark. + * + * Profiler::stop($token); + * + * @param string token + * @return void + */ + public static function stop($token) + { + // Stop the benchmark + Profiler::$_marks[$token]['stop_time'] = microtime(TRUE); + Profiler::$_marks[$token]['stop_memory'] = memory_get_usage(); + } + + /** + * Deletes a benchmark. If an error occurs during the benchmark, it is + * recommended to delete the benchmark to prevent statistics from being + * adversely affected. + * + * Profiler::delete($token); + * + * @param string token + * @return void + */ + public static function delete($token) + { + // Remove the benchmark + unset(Profiler::$_marks[$token]); + } + + /** + * Returns all the benchmark tokens by group and name as an array. + * + * $groups = Profiler::groups(); + * + * @return array + */ + public static function groups() + { + $groups = array(); + + foreach (Profiler::$_marks as $token => $mark) + { + // Sort the tokens by the group and name + $groups[$mark['group']][$mark['name']][] = $token; + } + + return $groups; + } + + /** + * Gets the min, max, average and total of a set of tokens as an array. + * + * $stats = Profiler::stats($tokens); + * + * @param array profiler tokens + * @return array min, max, average, total + * @uses Profiler::total + */ + public static function stats(array $tokens) + { + // Min and max are unknown by default + $min = $max = array( + 'time' => NULL, + 'memory' => NULL); + + // Total values are always integers + $total = array( + 'time' => 0, + 'memory' => 0); + + foreach ($tokens as $token) + { + // Get the total time and memory for this benchmark + list($time, $memory) = Profiler::total($token); + + if ($max['time'] === NULL OR $time > $max['time']) + { + // Set the maximum time + $max['time'] = $time; + } + + if ($min['time'] === NULL OR $time < $min['time']) + { + // Set the minimum time + $min['time'] = $time; + } + + // Increase the total time + $total['time'] += $time; + + if ($max['memory'] === NULL OR $memory > $max['memory']) + { + // Set the maximum memory + $max['memory'] = $memory; + } + + if ($min['memory'] === NULL OR $memory < $min['memory']) + { + // Set the minimum memory + $min['memory'] = $memory; + } + + // Increase the total memory + $total['memory'] += $memory; + } + + // Determine the number of tokens + $count = count($tokens); + + // Determine the averages + $average = array( + 'time' => $total['time'] / $count, + 'memory' => $total['memory'] / $count); + + return array( + 'min' => $min, + 'max' => $max, + 'total' => $total, + 'average' => $average); + } + + /** + * Gets the min, max, average and total of profiler groups as an array. + * + * $stats = Profiler::group_stats('test'); + * + * @param mixed single group name string, or array with group names; all groups by default + * @return array min, max, average, total + * @uses Profiler::groups + * @uses Profiler::stats + */ + public static function group_stats($groups = NULL) + { + // Which groups do we need to calculate stats for? + $groups = ($groups === NULL) + ? Profiler::groups() + : array_intersect_key(Profiler::groups(), array_flip((array) $groups)); + + // All statistics + $stats = array(); + + foreach ($groups as $group => $names) + { + foreach ($names as $name => $tokens) + { + // Store the stats for each subgroup. + // We only need the values for "total". + $_stats = Profiler::stats($tokens); + $stats[$group][$name] = $_stats['total']; + } + } + + // Group stats + $groups = array(); + + foreach ($stats as $group => $names) + { + // Min and max are unknown by default + $groups[$group]['min'] = $groups[$group]['max'] = array( + 'time' => NULL, + 'memory' => NULL); + + // Total values are always integers + $groups[$group]['total'] = array( + 'time' => 0, + 'memory' => 0); + + foreach ($names as $total) + { + if ( ! isset($groups[$group]['min']['time']) OR $groups[$group]['min']['time'] > $total['time']) + { + // Set the minimum time + $groups[$group]['min']['time'] = $total['time']; + } + if ( ! isset($groups[$group]['min']['memory']) OR $groups[$group]['min']['memory'] > $total['memory']) + { + // Set the minimum memory + $groups[$group]['min']['memory'] = $total['memory']; + } + + if ( ! isset($groups[$group]['max']['time']) OR $groups[$group]['max']['time'] < $total['time']) + { + // Set the maximum time + $groups[$group]['max']['time'] = $total['time']; + } + if ( ! isset($groups[$group]['max']['memory']) OR $groups[$group]['max']['memory'] < $total['memory']) + { + // Set the maximum memory + $groups[$group]['max']['memory'] = $total['memory']; + } + + // Increase the total time and memory + $groups[$group]['total']['time'] += $total['time']; + $groups[$group]['total']['memory'] += $total['memory']; + } + + // Determine the number of names (subgroups) + $count = count($names); + + // Determine the averages + $groups[$group]['average']['time'] = $groups[$group]['total']['time'] / $count; + $groups[$group]['average']['memory'] = $groups[$group]['total']['memory'] / $count; + } + + return $groups; + } + + /** + * Gets the total execution time and memory usage of a benchmark as a list. + * + * list($time, $memory) = Profiler::total($token); + * + * @param string token + * @return array execution time, memory + */ + public static function total($token) + { + // Import the benchmark data + $mark = Profiler::$_marks[$token]; + + if ($mark['stop_time'] === FALSE) + { + // The benchmark has not been stopped yet + $mark['stop_time'] = microtime(TRUE); + $mark['stop_memory'] = memory_get_usage(); + } + + return array + ( + // Total time in seconds + $mark['stop_time'] - $mark['start_time'], + + // Amount of memory in bytes + $mark['stop_memory'] - $mark['start_memory'], + ); + } + + /** + * Gets the total application run time and memory usage. Caches the result + * so that it can be compared between requests. + * + * list($time, $memory) = Profiler::application(); + * + * @return array execution time, memory + * @uses Kohana::cache + */ + public static function application() + { + // Load the stats from cache, which is valid for 1 day + $stats = Kohana::cache('profiler_application_stats', NULL, 3600 * 24); + + if ( ! is_array($stats) OR $stats['count'] > Profiler::$rollover) + { + // Initialize the stats array + $stats = array( + 'min' => array( + 'time' => NULL, + 'memory' => NULL), + 'max' => array( + 'time' => NULL, + 'memory' => NULL), + 'total' => array( + 'time' => NULL, + 'memory' => NULL), + 'count' => 0); + } + + // Get the application run time + $time = microtime(TRUE) - KOHANA_START_TIME; + + // Get the total memory usage + $memory = memory_get_usage() - KOHANA_START_MEMORY; + + // Calculate max time + if ($stats['max']['time'] === NULL OR $time > $stats['max']['time']) + $stats['max']['time'] = $time; + + // Calculate min time + if ($stats['min']['time'] === NULL OR $time < $stats['min']['time']) + $stats['min']['time'] = $time; + + // Add to total time + $stats['total']['time'] += $time; + + // Calculate max memory + if ($stats['max']['memory'] === NULL OR $memory > $stats['max']['memory']) + $stats['max']['memory'] = $memory; + + // Calculate min memory + if ($stats['min']['memory'] === NULL OR $memory < $stats['min']['memory']) + $stats['min']['memory'] = $memory; + + // Add to total memory + $stats['total']['memory'] += $memory; + + // Another mark has been added to the stats + $stats['count']++; + + // Determine the averages + $stats['average'] = array( + 'time' => $stats['total']['time'] / $stats['count'], + 'memory' => $stats['total']['memory'] / $stats['count']); + + // Cache the new stats + Kohana::cache('profiler_application_stats', $stats); + + // Set the current application execution time and memory + // Do NOT cache these, they are specific to the current request only + $stats['current']['time'] = $time; + $stats['current']['memory'] = $memory; + + // Return the total application run time and memory usage + return $stats; + } + +} // End Profiler diff --git a/includes/kohana/system/classes/kohana/remote.php b/includes/kohana/system/classes/kohana/remote.php new file mode 100644 index 00000000..0a2c604b --- /dev/null +++ b/includes/kohana/system/classes/kohana/remote.php @@ -0,0 +1,152 @@ + 'Mozilla/5.0 (compatible; Kohana v3.0 +http://kohanaphp.com/)', + CURLOPT_CONNECTTIMEOUT => 5, + CURLOPT_TIMEOUT => 5, + ); + + /** + * Returns the output of a remote URL. Any [curl option](http://php.net/curl_setopt) + * may be used. + * + * // Do a simple GET request + * $data = Remote::get($url); + * + * // Do a POST request + * $data = Remote::get($url, array( + * CURLOPT_POST => TRUE, + * CURLOPT_POSTFIELDS => http_build_query($array), + * )); + * + * @param string remote URL + * @param array curl options + * @return string + * @throws Kohana_Exception + */ + public static function get($url, array $options = NULL) + { + if ($options === NULL) + { + // Use default options + $options = Remote::$default_options; + } + else + { + // Add default options + $options = $options + Remote::$default_options; + } + + // The transfer must always be returned + $options[CURLOPT_RETURNTRANSFER] = TRUE; + + // Open a new remote connection + $remote = curl_init($url); + + // Set connection options + if ( ! curl_setopt_array($remote, $options)) + { + throw new Kohana_Exception('Failed to set CURL options, check CURL documentation: :url', + array(':url' => 'http://php.net/curl_setopt_array')); + } + + // Get the response + $response = curl_exec($remote); + + // Get the response information + $code = curl_getinfo($remote, CURLINFO_HTTP_CODE); + + if ($code AND $code < 200 OR $code > 299) + { + $error = $response; + } + elseif ($response === FALSE) + { + $error = curl_error($remote); + } + + // Close the connection + curl_close($remote); + + if (isset($error)) + { + throw new Kohana_Exception('Error fetching remote :url [ status :code ] :error', + array(':url' => $url, ':code' => $code, ':error' => $error)); + } + + return $response; + } + + /** + * Returns the status code (200, 500, etc) for a URL. + * + * $status = Remote::status($url); + * + * @param string URL to check + * @return integer + */ + public static function status($url) + { + // Get the hostname and path + $url = parse_url($url); + + if (empty($url['path'])) + { + // Request the root document + $url['path'] = '/'; + } + + // Open a remote connection + $port = isset($url['port']) ? $url['port'] : 80; + $remote = fsockopen($url['host'], $port, $errno, $errstr, 5); + + if ( ! is_resource($remote)) + return FALSE; + + // Set CRLF + $CRLF = "\r\n"; + + // Send request + fwrite($remote, 'HEAD '.$url['path'].' HTTP/1.0'.$CRLF); + fwrite($remote, 'Host: '.$url['host'].$CRLF); + fwrite($remote, 'Connection: close'.$CRLF); + fwrite($remote, 'User-Agent: Kohana Framework (+http://kohanaphp.com/)'.$CRLF); + + // Send one more CRLF to terminate the headers + fwrite($remote, $CRLF); + + // Remote is offline + $response = FALSE; + + while ( ! feof($remote)) + { + // Get the line + $line = trim(fgets($remote, 512)); + + if ($line !== '' AND preg_match('#^HTTP/1\.[01] (\d{3})#', $line, $matches)) + { + // Response code found + $response = (int) $matches[1]; + break; + } + } + + // Close the connection + fclose($remote); + + return $response; + } + +} // End remote diff --git a/includes/kohana/system/classes/kohana/request.php b/includes/kohana/system/classes/kohana/request.php new file mode 100644 index 00000000..f94b880d --- /dev/null +++ b/includes/kohana/system/classes/kohana/request.php @@ -0,0 +1,1221 @@ + 'Continue', + 101 => 'Switching Protocols', + + // Success 2xx + 200 => 'OK', + 201 => 'Created', + 202 => 'Accepted', + 203 => 'Non-Authoritative Information', + 204 => 'No Content', + 205 => 'Reset Content', + 206 => 'Partial Content', + 207 => 'Multi-Status', + + // Redirection 3xx + 300 => 'Multiple Choices', + 301 => 'Moved Permanently', + 302 => 'Found', // 1.1 + 303 => 'See Other', + 304 => 'Not Modified', + 305 => 'Use Proxy', + // 306 is deprecated but reserved + 307 => 'Temporary Redirect', + + // Client Error 4xx + 400 => 'Bad Request', + 401 => 'Unauthorized', + 402 => 'Payment Required', + 403 => 'Forbidden', + 404 => 'Not Found', + 405 => 'Method Not Allowed', + 406 => 'Not Acceptable', + 407 => 'Proxy Authentication Required', + 408 => 'Request Timeout', + 409 => 'Conflict', + 410 => 'Gone', + 411 => 'Length Required', + 412 => 'Precondition Failed', + 413 => 'Request Entity Too Large', + 414 => 'Request-URI Too Long', + 415 => 'Unsupported Media Type', + 416 => 'Requested Range Not Satisfiable', + 417 => 'Expectation Failed', + 422 => 'Unprocessable Entity', + 423 => 'Locked', + 424 => 'Failed Dependency', + + // Server Error 5xx + 500 => 'Internal Server Error', + 501 => 'Not Implemented', + 502 => 'Bad Gateway', + 503 => 'Service Unavailable', + 504 => 'Gateway Timeout', + 505 => 'HTTP Version Not Supported', + 507 => 'Insufficient Storage', + 509 => 'Bandwidth Limit Exceeded' + ); + + /** + * @var string method: GET, POST, PUT, DELETE, etc + */ + public static $method = 'GET'; + + /** + * @var string protocol: http, https, ftp, cli, etc + */ + public static $protocol = 'http'; + + /** + * @var string referring URL + */ + public static $referrer; + + /** + * @var string client user agent + */ + public static $user_agent = ''; + + /** + * @var string client IP address + */ + public static $client_ip = '0.0.0.0'; + + /** + * @var boolean AJAX-generated request + */ + public static $is_ajax = FALSE; + + /** + * @var object main request instance + */ + public static $instance; + + /** + * @var object currently executing request instance + */ + public static $current; + + /** + * Main request singleton instance. If no URI is provided, the URI will + * be automatically detected. + * + * $request = Request::instance(); + * + * @param string URI of the request + * @return Request + * @uses Request::detect_uri + */ + public static function instance( & $uri = TRUE) + { + if ( ! Request::$instance) + { + if (Kohana::$is_cli) + { + // Default protocol for command line is cli:// + Request::$protocol = 'cli'; + + // Get the command line options + $options = CLI::options('uri', 'method', 'get', 'post'); + + if (isset($options['uri'])) + { + // Use the specified URI + $uri = $options['uri']; + } + + if (isset($options['method'])) + { + // Use the specified method + Request::$method = strtoupper($options['method']); + } + + if (isset($options['get'])) + { + // Overload the global GET data + parse_str($options['get'], $_GET); + } + + if (isset($options['post'])) + { + // Overload the global POST data + parse_str($options['post'], $_POST); + } + } + else + { + if (isset($_SERVER['REQUEST_METHOD'])) + { + // Use the server request method + Request::$method = $_SERVER['REQUEST_METHOD']; + } + + if ( ! empty($_SERVER['HTTPS']) AND filter_var($_SERVER['HTTPS'], FILTER_VALIDATE_BOOLEAN)) + { + // This request is secure + Request::$protocol = 'https'; + } + + if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) AND strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') + { + // This request is an AJAX request + Request::$is_ajax = TRUE; + } + + if (isset($_SERVER['HTTP_REFERER'])) + { + // There is a referrer for this request + Request::$referrer = $_SERVER['HTTP_REFERER']; + } + + if (isset($_SERVER['HTTP_USER_AGENT'])) + { + // Set the client user agent + Request::$user_agent = $_SERVER['HTTP_USER_AGENT']; + } + + if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) + { + // Use the forwarded IP address, typically set when the + // client is using a proxy server. + Request::$client_ip = $_SERVER['HTTP_X_FORWARDED_FOR']; + } + elseif (isset($_SERVER['HTTP_CLIENT_IP'])) + { + // Use the forwarded IP address, typically set when the + // client is using a proxy server. + Request::$client_ip = $_SERVER['HTTP_CLIENT_IP']; + } + elseif (isset($_SERVER['REMOTE_ADDR'])) + { + // The remote IP address + Request::$client_ip = $_SERVER['REMOTE_ADDR']; + } + + if (Request::$method !== 'GET' AND Request::$method !== 'POST') + { + // Methods besides GET and POST do not properly parse the form-encoded + // query string into the $_POST array, so we overload it manually. + parse_str(file_get_contents('php://input'), $_POST); + } + + if ($uri === TRUE) + { + $uri = Request::detect_uri(); + } + } + + // Reduce multiple slashes to a single slash + $uri = preg_replace('#//+#', '/', $uri); + + // Remove all dot-paths from the URI, they are not valid + $uri = preg_replace('#\.[\s./]*/#', '', $uri); + + // Create the instance singleton + Request::$instance = Request::$current = new Request($uri); + + // Add the default Content-Type header + Request::$instance->headers['Content-Type'] = 'text/html; charset='.Kohana::$charset; + } + + return Request::$instance; + } + + /** + * Automatically detects the URI of the main request using PATH_INFO, + * REQUEST_URI, PHP_SELF or REDIRECT_URL. + * + * $uri = Request::detect_uri(); + * + * @return string URI of the main request + * @throws Kohana_Exception + * @since 3.0.8 + */ + public static function detect_uri() + { + if ( ! empty($_SERVER['PATH_INFO'])) + { + // PATH_INFO does not contain the docroot or index + $uri = $_SERVER['PATH_INFO']; + } + else + { + // REQUEST_URI and PHP_SELF include the docroot and index + + if (isset($_SERVER['REQUEST_URI'])) + { + // REQUEST_URI includes the query string, remove it + $uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); + + // Decode the request URI + $uri = rawurldecode($uri); + } + elseif (isset($_SERVER['PHP_SELF'])) + { + $uri = $_SERVER['PHP_SELF']; + } + elseif (isset($_SERVER['REDIRECT_URL'])) + { + $uri = $_SERVER['REDIRECT_URL']; + } + else + { + // If you ever see this error, please report an issue at http://dev.kohanaphp.com/projects/kohana3/issues + // along with any relevant information about your web server setup. Thanks! + throw new Kohana_Exception('Unable to detect the URI using PATH_INFO, REQUEST_URI, PHP_SELF or REDIRECT_URL'); + } + + // Get the path from the base URL, including the index file + $base_url = parse_url(Kohana::$base_url, PHP_URL_PATH); + + if (strpos($uri, $base_url) === 0) + { + // Remove the base URL from the URI + $uri = substr($uri, strlen($base_url)); + } + + if (Kohana::$index_file AND strpos($uri, Kohana::$index_file) === 0) + { + // Remove the index file from the URI + $uri = substr($uri, strlen(Kohana::$index_file)); + } + } + + return $uri; + } + + /** + * Return the currently executing request. This is changed to the current + * request when [Request::execute] is called and restored when the request + * is completed. + * + * $request = Request::current(); + * + * @return Request + * @since 3.0.5 + */ + public static function current() + { + return Request::$current; + } + + /** + * Creates a new request object for the given URI. This differs from + * [Request::instance] in that it does not automatically detect the URI + * and should only be used for creating HMVC requests. + * + * $request = Request::factory($uri); + * + * @param string URI of the request + * @return Request + */ + public static function factory($uri) + { + return new Request($uri); + } + + /** + * Returns information about the client user agent. + * + * // Returns "Chrome" when using Google Chrome + * $browser = Request::user_agent('browser'); + * + * Multiple values can be returned at once by using an array: + * + * // Get the browser and platform with a single call + * $info = Request::user_agent(array('browser', 'platform')); + * + * When using an array for the value, an associative array will be returned. + * + * @param mixed string to return: browser, version, robot, mobile, platform; or array of values + * @return mixed requested information, FALSE if nothing is found + * @uses Kohana::config + * @uses Request::$user_agent + */ + public static function user_agent($value) + { + if (is_array($value)) + { + $agent = array(); + foreach ($value as $v) + { + // Add each key to the set + $agent[$v] = Request::user_agent($v); + } + + return $agent; + } + + static $info; + + if (isset($info[$value])) + { + // This value has already been found + return $info[$value]; + } + + if ($value === 'browser' OR $value == 'version') + { + // Load browsers + $browsers = Kohana::config('user_agents')->browser; + + foreach ($browsers as $search => $name) + { + if (stripos(Request::$user_agent, $search) !== FALSE) + { + // Set the browser name + $info['browser'] = $name; + + if (preg_match('#'.preg_quote($search).'[^0-9.]*+([0-9.][0-9.a-z]*)#i', Request::$user_agent, $matches)) + { + // Set the version number + $info['version'] = $matches[1]; + } + else + { + // No version number found + $info['version'] = FALSE; + } + + return $info[$value]; + } + } + } + else + { + // Load the search group for this type + $group = Kohana::config('user_agents')->$value; + + foreach ($group as $search => $name) + { + if (stripos(Request::$user_agent, $search) !== FALSE) + { + // Set the value name + return $info[$value] = $name; + } + } + } + + // The value requested could not be found + return $info[$value] = FALSE; + } + + /** + * Returns the accepted content types. If a specific type is defined, + * the quality of that type will be returned. + * + * $types = Request::accept_type(); + * + * @param string content MIME type + * @return float when checking a specific type + * @return array + * @uses Request::_parse_accept + */ + public static function accept_type($type = NULL) + { + static $accepts; + + if ($accepts === NULL) + { + // Parse the HTTP_ACCEPT header + $accepts = Request::_parse_accept($_SERVER['HTTP_ACCEPT'], array('*/*' => 1.0)); + } + + if (isset($type)) + { + // Return the quality setting for this type + return isset($accepts[$type]) ? $accepts[$type] : $accepts['*/*']; + } + + return $accepts; + } + + /** + * Returns the accepted languages. If a specific language is defined, + * the quality of that language will be returned. If the language is not + * accepted, FALSE will be returned. + * + * $langs = Request::accept_lang(); + * + * @param string language code + * @return float when checking a specific language + * @return array + * @uses Request::_parse_accept + */ + public static function accept_lang($lang = NULL) + { + static $accepts; + + if ($accepts === NULL) + { + // Parse the HTTP_ACCEPT_LANGUAGE header + $accepts = Request::_parse_accept($_SERVER['HTTP_ACCEPT_LANGUAGE']); + } + + if (isset($lang)) + { + // Return the quality setting for this lang + return isset($accepts[$lang]) ? $accepts[$lang] : FALSE; + } + + return $accepts; + } + + /** + * Returns the accepted encodings. If a specific encoding is defined, + * the quality of that encoding will be returned. If the encoding is not + * accepted, FALSE will be returned. + * + * $encodings = Request::accept_encoding(); + * + * @param string encoding type + * @return float when checking a specific encoding + * @return array + * @uses Request::_parse_accept + */ + public static function accept_encoding($type = NULL) + { + static $accepts; + + if ($accepts === NULL) + { + // Parse the HTTP_ACCEPT_LANGUAGE header + $accepts = Request::_parse_accept($_SERVER['HTTP_ACCEPT_ENCODING']); + } + + if (isset($type)) + { + // Return the quality setting for this type + return isset($accepts[$type]) ? $accepts[$type] : FALSE; + } + + return $accepts; + } + + /** + * Parses an accept header and returns an array (type => quality) of the + * accepted types, ordered by quality. + * + * $accept = Request::_parse_accept($header, $defaults); + * + * @param string header to parse + * @param array default values + * @return array + */ + protected static function _parse_accept( & $header, array $accepts = NULL) + { + if ( ! empty($header)) + { + // Get all of the types + $types = explode(',', $header); + + foreach ($types as $type) + { + // Split the type into parts + $parts = explode(';', $type); + + // Make the type only the MIME + $type = trim(array_shift($parts)); + + // Default quality is 1.0 + $quality = 1.0; + + foreach ($parts as $part) + { + // Prevent undefined $value notice below + if (strpos($part, '=') === FALSE) + continue; + + // Separate the key and value + list ($key, $value) = explode('=', trim($part)); + + if ($key === 'q') + { + // There is a quality for this type + $quality = (float) trim($value); + } + } + + // Add the accept type and quality + $accepts[$type] = $quality; + } + } + + // Make sure that accepts is an array + $accepts = (array) $accepts; + + // Order by quality + arsort($accepts); + + return $accepts; + } + + /** + * @var object route matched for this request + */ + public $route; + + /** + * @var integer HTTP response code: 200, 404, 500, etc + */ + public $status = 200; + + /** + * @var string response body + */ + public $response = ''; + + /** + * @var array headers to send with the response body + */ + public $headers = array(); + + /** + * @var string controller directory + */ + public $directory = ''; + + /** + * @var string controller to be executed + */ + public $controller; + + /** + * @var string action to be executed in the controller + */ + public $action; + + /** + * @var string the URI of the request + */ + public $uri; + + // Parameters extracted from the route + protected $_params; + + /** + * Creates a new request object for the given URI. New requests should be + * created using the [Request::instance] or [Request::factory] methods. + * + * $request = new Request($uri); + * + * @param string URI of the request + * @return void + * @throws Kohana_Request_Exception + * @uses Route::all + * @uses Route::matches + */ + public function __construct($uri) + { + // Remove trailing slashes from the URI + $uri = trim($uri, '/'); + + // Load routes + $routes = Route::all(); + + foreach ($routes as $name => $route) + { + if ($params = $route->matches($uri)) + { + // Store the URI + $this->uri = $uri; + + // Store the matching route + $this->route = $route; + + if (isset($params['directory'])) + { + // Controllers are in a sub-directory + $this->directory = $params['directory']; + } + + // Store the controller + $this->controller = $params['controller']; + + if (isset($params['action'])) + { + // Store the action + $this->action = $params['action']; + } + else + { + // Use the default action + $this->action = Route::$default_action; + } + + // These are accessible as public vars and can be overloaded + unset($params['controller'], $params['action'], $params['directory']); + + // Params cannot be changed once matched + $this->_params = $params; + + return; + } + } + + // No matching route for this URI + $this->status = 404; + + throw new Kohana_Request_Exception('Unable to find a route to match the URI: :uri', + array(':uri' => $uri)); + } + + /** + * Returns the response as the string representation of a request. + * + * echo $request; + * + * @return string + */ + public function __toString() + { + return (string) $this->response; + } + + /** + * Generates a relative URI for the current route. + * + * $request->uri($params); + * + * @param array additional route parameters + * @return string + * @uses Route::uri + */ + public function uri(array $params = NULL) + { + if ( ! isset($params['directory'])) + { + // Add the current directory + $params['directory'] = $this->directory; + } + + if ( ! isset($params['controller'])) + { + // Add the current controller + $params['controller'] = $this->controller; + } + + if ( ! isset($params['action'])) + { + // Add the current action + $params['action'] = $this->action; + } + + // Add the current parameters + $params += $this->_params; + + return $this->route->uri($params); + } + + /** + * Create a URL from the current request. This is a shortcut for: + * + * echo URL::site($this->request->uri($params), $protocol); + * + * @param string route name + * @param array URI parameters + * @param mixed protocol string or boolean, adds protocol and domain + * @return string + * @since 3.0.7 + * @uses URL::site + */ + public function url(array $params = NULL, $protocol = NULL) + { + // Create a URI with the current route and convert it to a URL + return URL::site($this->uri($params), $protocol); + } + + /** + * Retrieves a value from the route parameters. + * + * $id = $request->param('id'); + * + * @param string key of the value + * @param mixed default value if the key is not set + * @return mixed + */ + public function param($key = NULL, $default = NULL) + { + if ($key === NULL) + { + // Return the full array + return $this->_params; + } + + return isset($this->_params[$key]) ? $this->_params[$key] : $default; + } + + /** + * Sends the response status and all set headers. The current server + * protocol (HTTP/1.0 or HTTP/1.1) will be used when available. If not + * available, HTTP/1.1 will be used. + * + * $request->send_headers(); + * + * @return $this + * @uses Request::$messages + */ + public function send_headers() + { + if ( ! headers_sent()) + { + if (isset($_SERVER['SERVER_PROTOCOL'])) + { + // Use the default server protocol + $protocol = $_SERVER['SERVER_PROTOCOL']; + } + else + { + // Default to using newer protocol + $protocol = 'HTTP/1.1'; + } + + // HTTP status line + header($protocol.' '.$this->status.' '.Request::$messages[$this->status]); + + foreach ($this->headers as $name => $value) + { + if (is_string($name)) + { + // Combine the name and value to make a raw header + $value = "{$name}: {$value}"; + } + + // Send the raw header + header($value, TRUE); + } + } + + return $this; + } + + /** + * Redirects as the request response. If the URL does not include a + * protocol, it will be converted into a complete URL. + * + * $request->redirect($url); + * + * [!!] No further processing can be done after this method is called! + * + * @param string redirect location + * @param integer status code: 301, 302, etc + * @return void + * @uses URL::site + * @uses Request::send_headers + */ + public function redirect($url, $code = 302) + { + if (strpos($url, '://') === FALSE) + { + // Make the URI into a URL + $url = URL::site($url, TRUE); + } + + // Set the response status + $this->status = $code; + + // Set the location header + $this->headers['Location'] = $url; + + // Send headers + $this->send_headers(); + + // Stop execution + exit; + } + + /** + * Send file download as the response. All execution will be halted when + * this method is called! Use TRUE for the filename to send the current + * response as the file content. The third parameter allows the following + * options to be set: + * + * Type | Option | Description | Default Value + * ----------|-----------|------------------------------------|-------------- + * `boolean` | inline | Display inline instead of download | `FALSE` + * `string` | mime_type | Manual mime type | Automatic + * `boolean` | delete | Delete the file after sending | `FALSE` + * + * Download a file that already exists: + * + * $request->send_file('media/packages/kohana.zip'); + * + * Download generated content as a file: + * + * $request->response = $content; + * $request->send_file(TRUE, $filename); + * + * [!!] No further processing can be done after this method is called! + * + * @param string filename with path, or TRUE for the current response + * @param string downloaded file name + * @param array additional options + * @return void + * @throws Kohana_Exception + * @uses File::mime_by_ext + * @uses File::mime + * @uses Request::send_headers + */ + public function send_file($filename, $download = NULL, array $options = NULL) + { + if ( ! empty($options['mime_type'])) + { + // The mime-type has been manually set + $mime = $options['mime_type']; + } + + if ($filename === TRUE) + { + if (empty($download)) + { + throw new Kohana_Exception('Download name must be provided for streaming files'); + } + + // Temporary files will automatically be deleted + $options['delete'] = FALSE; + + if ( ! isset($mime)) + { + // Guess the mime using the file extension + $mime = File::mime_by_ext(strtolower(pathinfo($download, PATHINFO_EXTENSION))); + } + + // Force the data to be rendered if + $file_data = (string) $this->response; + + // Get the content size + $size = strlen($file_data); + + // Create a temporary file to hold the current response + $file = tmpfile(); + + // Write the current response into the file + fwrite($file, $file_data); + + // Prepare the file for reading + fseek($file, 0); + + // File data is no longer needed + unset($file_data); + } + else + { + // Get the complete file path + $filename = realpath($filename); + + if (empty($download)) + { + // Use the file name as the download file name + $download = pathinfo($filename, PATHINFO_BASENAME); + } + + // Get the file size + $size = filesize($filename); + + if ( ! isset($mime)) + { + // Get the mime type + $mime = File::mime($filename); + } + + // Open the file for reading + $file = fopen($filename, 'rb'); + } + + if ( ! is_resource($file)) + { + throw new Kohana_Exception('Could not read file to send: :file', array( + ':file' => $download, + )); + } + + // Inline or download? + $disposition = empty($options['inline']) ? 'attachment' : 'inline'; + + // Set the headers for a download + $this->headers['Content-Disposition'] = $disposition.'; filename="'.$download.'"'; + $this->headers['Content-Type'] = $mime; + $this->headers['Content-Length'] = $size; + + if (Request::user_agent('browser') === 'Internet Explorer') + { + // Naturally, IE does not act like a real browser... + + if (Request::$protocol === 'https') + { + // http://support.microsoft.com/kb/316431 + $this->headers['Pragma'] = $this->headers['Cache-Control'] = 'public'; + } + + if (version_compare(Request::user_agent('version'), '8.0', '>=')) + { + // http://ajaxian.com/archives/ie-8-security + $this->headers['X-Content-Type-Options'] = 'nosniff'; + } + } + + if ( ! empty($options['resumable'])) + { + // @todo: ranged download processing + } + + // Send all headers now + $this->send_headers(); + + while (ob_get_level()) + { + // Flush all output buffers + ob_end_flush(); + } + + // Manually stop execution + ignore_user_abort(TRUE); + + // Keep the script running forever + set_time_limit(0); + + // Send data in 16kb blocks + $block = 1024 * 16; + + while ( ! feof($file)) + { + if (connection_aborted()) + break; + + // Output a block of the file + echo fread($file, $block); + + // Send the data now + flush(); + } + + // Close the file + fclose($file); + + if ( ! empty($options['delete'])) + { + try + { + // Attempt to remove the file + unlink($filename); + } + catch (Exception $e) + { + // Create a text version of the exception + $error = Kohana::exception_text($e); + + if (is_object(Kohana::$log)) + { + // Add this exception to the log + Kohana::$log->add(Kohana::ERROR, $error); + + // Make sure the logs are written + Kohana::$log->write(); + } + + // Do NOT display the exception, it will corrupt the output! + } + } + + // Stop execution + exit; + } + + /** + * Processes the request, executing the controller action that handles this + * request, determined by the [Route]. + * + * 1. Before the controller action is called, the [Controller::before] method + * will be called. + * 2. Next the controller action will be called. + * 3. After the controller action is called, the [Controller::after] method + * will be called. + * + * By default, the output from the controller is captured and returned, and + * no headers are sent. + * + * $request->execute(); + * + * @return $this + * @throws Kohana_Exception + * @uses [Kohana::$profiling] + * @uses [Profiler] + */ + public function execute() + { + // Create the class prefix + $prefix = 'controller_'; + + if ($this->directory) + { + // Add the directory name to the class prefix + $prefix .= str_replace(array('\\', '/'), '_', trim($this->directory, '/')).'_'; + } + + if (Kohana::$profiling) + { + // Set the benchmark name + $benchmark = '"'.$this->uri.'"'; + + if ($this !== Request::$instance AND Request::$current) + { + // Add the parent request uri + $benchmark .= ' « "'.Request::$current->uri.'"'; + } + + // Start benchmarking + $benchmark = Profiler::start('Requests', $benchmark); + } + + // Store the currently active request + $previous = Request::$current; + + // Change the current request to this request + Request::$current = $this; + + try + { + // Load the controller using reflection + $class = new ReflectionClass($prefix.$this->controller); + + if ($class->isAbstract()) + { + throw new Kohana_Exception('Cannot create instances of abstract :controller', + array(':controller' => $prefix.$this->controller)); + } + + // Create a new instance of the controller + $controller = $class->newInstance($this); + + // Execute the "before action" method + $class->getMethod('before')->invoke($controller); + + // Determine the action to use + $action = empty($this->action) ? Route::$default_action : $this->action; + + // Execute the main action with the parameters + $class->getMethod('action_'.$action)->invokeArgs($controller, $this->_params); + + // Execute the "after action" method + $class->getMethod('after')->invoke($controller); + } + catch (Exception $e) + { + // Restore the previous request + Request::$current = $previous; + + if (isset($benchmark)) + { + // Delete the benchmark, it is invalid + Profiler::delete($benchmark); + } + + if ($e instanceof ReflectionException) + { + // Reflection will throw exceptions for missing classes or actions + $this->status = 404; + } + else + { + // All other exceptions are PHP/server errors + $this->status = 500; + } + + // Re-throw the exception + throw $e; + } + + // Restore the previous request + Request::$current = $previous; + + if (isset($benchmark)) + { + // Stop the benchmark + Profiler::stop($benchmark); + } + + return $this; + } + + + /** + * Generates an [ETag](http://en.wikipedia.org/wiki/HTTP_ETag) from the + * request response. + * + * $etag = $request->generate_etag(); + * + * [!!] If the request response is empty when this method is called, an + * exception will be thrown! + * + * @return string + * @throws Kohana_Request_Exception + */ + public function generate_etag() + { + if ($this->response === NULL) + { + throw new Kohana_Request_Exception('No response yet associated with request - cannot auto generate resource ETag'); + } + + // Generate a unique hash for the response + return '"'.sha1($this->response).'"'; + } + + + /** + * Checks the browser cache to see the response needs to be returned. + * + * $request->check_cache($etag); + * + * [!!] If the cache check succeeds, no further processing can be done! + * + * @param string etag to check + * @return $this + * @throws Kohana_Request_Exception + * @uses Request::generate_etag + */ + public function check_cache($etag = null) + { + if (empty($etag)) + { + $etag = $this->generate_etag(); + } + + // Set the ETag header + $this->headers['ETag'] = $etag; + + // Add the Cache-Control header if it is not already set + // This allows etags to be used with Max-Age, etc + $this->headers += array( + 'Cache-Control' => 'must-revalidate', + ); + + if (isset($_SERVER['HTTP_IF_NONE_MATCH']) AND $_SERVER['HTTP_IF_NONE_MATCH'] === $etag) + { + // No need to send data again + $this->status = 304; + $this->send_headers(); + + // Stop execution + exit; + } + + return $this; + } + +} // End Request diff --git a/includes/kohana/system/classes/kohana/request/exception.php b/includes/kohana/system/classes/kohana/request/exception.php new file mode 100644 index 00000000..f66ea22d --- /dev/null +++ b/includes/kohana/system/classes/kohana/request/exception.php @@ -0,0 +1,9 @@ + will be translated to a regular expression using a default + * regular expression pattern. You can override the default pattern by providing + * a pattern for the key: + * + * // This route will only match when is a digit + * Route::set('user', 'user//', array('id' => '\d+')); + * + * // This route will match when is anything + * Route::set('file', '', array('path' => '.*')); + * + * It is also possible to create optional segments by using parentheses in + * the URI definition: + * + * // This is the standard default route, and no keys are required + * Route::set('default', '((/(/)))'); + * + * // This route only requires the key + * Route::set('file', '(/)(.)', array('path' => '.*', 'format' => '\w+')); + * + * Routes also provide a way to generate URIs (called "reverse routing"), which + * makes them an extremely powerful and flexible way to generate internal links. + * + * @package Kohana + * @category Base + * @author Kohana Team + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_Route { + + // Defines the pattern of a + const REGEX_KEY = '<([a-zA-Z0-9_]++)>'; + + // What can be part of a value + const REGEX_SEGMENT = '[^/.,;?\n]++'; + + // What must be escaped in the route regex + const REGEX_ESCAPE = '[.\\+*?[^\\]${}=!|]'; + + /** + * @var string default action for all routes + */ + public static $default_action = 'index'; + + // List of route objects + protected static $_routes = array(); + + /** + * Stores a named route and returns it. The "action" will always be set to + * "index" if it is not defined. + * + * Route::set('default', '((/(/)))') + * ->defaults(array( + * 'controller' => 'welcome', + * )); + * + * @param string route name + * @param string URI pattern + * @param array regex patterns for route keys + * @return Route + */ + public static function set($name, $uri, array $regex = NULL) + { + return Route::$_routes[$name] = new Route($uri, $regex); + } + + /** + * Retrieves a named route. + * + * $route = Route::get('default'); + * + * @param string route name + * @return Route + * @throws Kohana_Exception + */ + public static function get($name) + { + if ( ! isset(Route::$_routes[$name])) + { + throw new Kohana_Exception('The requested route does not exist: :route', + array(':route' => $name)); + } + + return Route::$_routes[$name]; + } + + /** + * Retrieves all named routes. + * + * $routes = Route::all(); + * + * @return array routes by name + */ + public static function all() + { + return Route::$_routes; + } + + /** + * Get the name of a route. + * + * $name = Route::name($route) + * + * @param object Route instance + * @return string + */ + public static function name(Route $route) + { + return array_search($route, Route::$_routes); + } + + /** + * Saves or loads the route cache. If your routes will remain the same for + * a long period of time, use this to reload the routes from the cache + * rather than redefining them on every page load. + * + * if ( ! Route::cache()) + * { + * // Set routes here + * Route::cache(TRUE); + * } + * + * @param boolean cache the current routes + * @return void when saving routes + * @return boolean when loading routes + * @uses Kohana::cache + */ + public static function cache($save = FALSE) + { + if ($save === TRUE) + { + // Cache all defined routes + Kohana::cache('Route::cache()', Route::$_routes); + } + else + { + if ($routes = Kohana::cache('Route::cache()')) + { + Route::$_routes = $routes; + + // Routes were cached + return TRUE; + } + else + { + // Routes were not cached + return FALSE; + } + } + } + + /** + * Create a URL from a route name. This is a shortcut for: + * + * echo URL::site(Route::get($name)->uri($params), $protocol); + * + * @param string route name + * @param array URI parameters + * @param mixed protocol string or boolean, adds protocol and domain + * @return string + * @since 3.0.7 + * @uses URL::site + */ + public static function url($name, array $params = NULL, $protocol = NULL) + { + // Create a URI with the route and convert it to a URL + return URL::site(Route::get($name)->uri($params), $protocol); + } + + // Route URI string + protected $_uri = ''; + + // Regular expressions for route keys + protected $_regex = array(); + + // Default values for route keys + protected $_defaults = array('action' => 'index'); + + // Compiled regex cache + protected $_route_regex; + + /** + * Creates a new route. Sets the URI and regular expressions for keys. + * Routes should always be created with [Route::set] or they will not + * be properly stored. + * + * $route = new Route($uri, $regex); + * + * @param string route URI pattern + * @param array key patterns + * @return void + * @uses Route::_compile + */ + public function __construct($uri = NULL, array $regex = NULL) + { + if ($uri === NULL) + { + // Assume the route is from cache + return; + } + + if ( ! empty($regex)) + { + $this->_regex = $regex; + } + + // Store the URI that this route will match + $this->_uri = $uri; + + // Store the compiled regex locally + $this->_route_regex = $this->_compile(); + } + + /** + * Provides default values for keys when they are not present. The default + * action will always be "index" unless it is overloaded here. + * + * $route->defaults(array( + * 'controller' => 'welcome', + * 'action' => 'index' + * )); + * + * @param array key values + * @return $this + */ + public function defaults(array $defaults = NULL) + { + $this->_defaults = $defaults; + + return $this; + } + + /** + * Tests if the route matches a given URI. A successful match will return + * all of the routed parameters as an array. A failed match will return + * boolean FALSE. + * + * // Params: controller = users, action = edit, id = 10 + * $params = $route->matches('users/edit/10'); + * + * This method should almost always be used within an if/else block: + * + * if ($params = $route->matches($uri)) + * { + * // Parse the parameters + * } + * + * @param string URI to match + * @return array on success + * @return FALSE on failure + */ + public function matches($uri) + { + if ( ! preg_match($this->_route_regex, $uri, $matches)) + return FALSE; + + $params = array(); + foreach ($matches as $key => $value) + { + if (is_int($key)) + { + // Skip all unnamed keys + continue; + } + + // Set the value for all matched keys + $params[$key] = $value; + } + + foreach ($this->_defaults as $key => $value) + { + if ( ! isset($params[$key]) OR $params[$key] === '') + { + // Set default values for any key that was not matched + $params[$key] = $value; + } + } + + return $params; + } + + /** + * Generates a URI for the current route based on the parameters given. + * + * // Using the "default" route: "users/profile/10" + * $route->uri(array( + * 'controller' => 'users', + * 'action' => 'profile', + * 'id' => '10' + * )); + * + * @param array URI parameters + * @return string + * @throws Kohana_Exception + * @uses Route::REGEX_Key + */ + public function uri(array $params = NULL) + { + if ($params === NULL) + { + // Use the default parameters + $params = $this->_defaults; + } + else + { + // Add the default parameters + $params += $this->_defaults; + } + + // Start with the routed URI + $uri = $this->_uri; + + if (strpos($uri, '<') === FALSE AND strpos($uri, '(') === FALSE) + { + // This is a static route, no need to replace anything + return $uri; + } + + while (preg_match('#\([^()]++\)#', $uri, $match)) + { + // Search for the matched value + $search = $match[0]; + + // Remove the parenthesis from the match as the replace + $replace = substr($match[0], 1, -1); + + while(preg_match('#'.Route::REGEX_KEY.'#', $replace, $match)) + { + list($key, $param) = $match; + + if (isset($params[$param])) + { + // Replace the key with the parameter value + $replace = str_replace($key, $params[$param], $replace); + } + else + { + // This group has missing parameters + $replace = ''; + break; + } + } + + // Replace the group in the URI + $uri = str_replace($search, $replace, $uri); + } + + while(preg_match('#'.Route::REGEX_KEY.'#', $uri, $match)) + { + list($key, $param) = $match; + + if ( ! isset($params[$param])) + { + // Ungrouped parameters are required + throw new Kohana_Exception('Required route parameter not passed: :param', + array(':param' => $param)); + } + + $uri = str_replace($key, $params[$param], $uri); + } + + // Trim all extra slashes from the URI + $uri = preg_replace('#//+#', '/', rtrim($uri, '/')); + + return $uri; + } + + /** + * Returns the compiled regular expression for the route. This translates + * keys and optional groups to a proper PCRE regular expression. + * + * $regex = $route->_compile(); + * + * @return string + * @uses Route::REGEX_ESCAPE + * @uses Route::REGEX_SEGMENT + */ + protected function _compile() + { + // The URI should be considered literal except for keys and optional parts + // Escape everything preg_quote would escape except for : ( ) < > + $regex = preg_replace('#'.Route::REGEX_ESCAPE.'#', '\\\\$0', $this->_uri); + + if (strpos($regex, '(') !== FALSE) + { + // Make optional parts of the URI non-capturing and optional + $regex = str_replace(array('(', ')'), array('(?:', ')?'), $regex); + } + + // Insert default regex for keys + $regex = str_replace(array('<', '>'), array('(?P<', '>'.Route::REGEX_SEGMENT.')'), $regex); + + if ( ! empty($this->_regex)) + { + $search = $replace = array(); + foreach ($this->_regex as $key => $value) + { + $search[] = "<$key>".Route::REGEX_SEGMENT; + $replace[] = "<$key>$value"; + } + + // Replace the default regex with the user-specified regex + $regex = str_replace($search, $replace, $regex); + } + + return '#^'.$regex.'$#uD'; + } + +} // End Route diff --git a/includes/kohana/system/classes/kohana/security.php b/includes/kohana/system/classes/kohana/security.php new file mode 100644 index 00000000..16bfd2c8 --- /dev/null +++ b/includes/kohana/system/classes/kohana/security.php @@ -0,0 +1,193 @@ + + * @copyright (c) 2001-2006 Bitflux GmbH + * @param mixed string or array to sanitize + * @return string + * @deprecated since v3.0.5 + */ + public static function xss_clean($str) + { + // http://svn.bitflux.ch/repos/public/popoon/trunk/classes/externalinput.php + // +----------------------------------------------------------------------+ + // | Copyright (c) 2001-2006 Bitflux GmbH | + // +----------------------------------------------------------------------+ + // | Licensed under the Apache License, Version 2.0 (the "License"); | + // | you may not use this file except in compliance with the License. | + // | You may obtain a copy of the License at | + // | http://www.apache.org/licenses/LICENSE-2.0 | + // | Unless required by applicable law or agreed to in writing, software | + // | distributed under the License is distributed on an "AS IS" BASIS, | + // | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | + // | implied. See the License for the specific language governing | + // | permissions and limitations under the License. | + // +----------------------------------------------------------------------+ + // | Author: Christian Stocker | + // +----------------------------------------------------------------------+ + // + // Kohana Modifications: + // * Changed double quotes to single quotes, changed indenting and spacing + // * Removed magic_quotes stuff + // * Increased regex readability: + // * Used delimeters that aren't found in the pattern + // * Removed all unneeded escapes + // * Deleted U modifiers and swapped greediness where needed + // * Increased regex speed: + // * Made capturing parentheses non-capturing where possible + // * Removed parentheses where possible + // * Split up alternation alternatives + // * Made some quantifiers possessive + // * Handle arrays recursively + + if (is_array($str) OR is_object($str)) + { + foreach ($str as $k => $s) + { + $str[$k] = Security::xss_clean($s); + } + + return $str; + } + + // Remove all NULL bytes + $str = str_replace("\0", '', $str); + + // Fix &entity\n; + $str = str_replace(array('&','<','>'), array('&amp;','&lt;','&gt;'), $str); + $str = preg_replace('/(&#*\w+)[\x00-\x20]+;/u', '$1;', $str); + $str = preg_replace('/(&#x*[0-9A-F]+);*/iu', '$1;', $str); + $str = html_entity_decode($str, ENT_COMPAT, Kohana::$charset); + + // Remove any attribute starting with "on" or xmlns + $str = preg_replace('#(?:on[a-z]+|xmlns)\s*=\s*[\'"\x00-\x20]?[^\'>"]*[\'"\x00-\x20]?\s?#iu', '', $str); + + // Remove javascript: and vbscript: protocols + $str = preg_replace('#([a-z]*)[\x00-\x20]*=[\x00-\x20]*([`\'"]*)[\x00-\x20]*j[\x00-\x20]*a[\x00-\x20]*v[\x00-\x20]*a[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu', '$1=$2nojavascript...', $str); + $str = preg_replace('#([a-z]*)[\x00-\x20]*=([\'"]*)[\x00-\x20]*v[\x00-\x20]*b[\x00-\x20]*s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:#iu', '$1=$2novbscript...', $str); + $str = preg_replace('#([a-z]*)[\x00-\x20]*=([\'"]*)[\x00-\x20]*-moz-binding[\x00-\x20]*:#u', '$1=$2nomozbinding...', $str); + + // Only works in IE: + $str = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?expression[\x00-\x20]*\([^>]*+>#is', '$1>', $str); + $str = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?behaviour[\x00-\x20]*\([^>]*+>#is', '$1>', $str); + $str = preg_replace('#(<[^>]+?)style[\x00-\x20]*=[\x00-\x20]*[`\'"]*.*?s[\x00-\x20]*c[\x00-\x20]*r[\x00-\x20]*i[\x00-\x20]*p[\x00-\x20]*t[\x00-\x20]*:*[^>]*+>#ius', '$1>', $str); + + // Remove namespaced elements (we do not need them) + $str = preg_replace('#]*+>#i', '', $str); + + do + { + // Remove really unwanted tags + $old = $str; + $str = preg_replace('#]*+>#i', '', $str); + } + while ($old !== $str); + + return $str; + } + + /** + * Generate and store a unique token which can be used to help prevent + * [CSRF](http://wikipedia.org/wiki/Cross_Site_Request_Forgery) attacks. + * + * $token = Security::token(); + * + * You can insert this token into your forms as a hidden field: + * + * echo Form::hidden('csrf', Security::token()); + * + * And then check it when using [Validate]: + * + * $array->rules('csrf', array( + * 'not_empty' => NULL, + * 'Security::check' => NULL, + * )); + * + * This provides a basic, but effective, method of preventing CSRF attacks. + * + * @param boolean force a new token to be generated? + * @return string + * @uses Session::instance + */ + public static function token($new = FALSE) + { + $session = Session::instance(); + + // Get the current token + $token = $session->get(Security::$token_name); + + if ($new === TRUE OR ! $token) + { + // Generate a new unique token + $token = uniqid('security'); + + // Store the new token + $session->set(Security::$token_name, $token); + } + + return $token; + } + + /** + * Check that the given token matches the currently stored security token. + * + * if (Security::check($token)) + * { + * // Pass + * } + * + * @param string token to check + * @return boolean + * @uses Security::token + */ + public static function check($token) + { + return Security::token() === $token; + } + + /** + * Remove image tags from a string. + * + * $str = Security::strip_image_tags($str); + * + * @param string string to sanitize + * @return string + */ + public static function strip_image_tags($str) + { + return preg_replace('#\s]*)["\']?[^>]*)?>#is', '$1', $str); + } + + /** + * Encodes PHP tags in a string. + * + * $str = Security::encode_php_tags($str); + * + * @param string string to sanitize + * @return string + */ + public static function encode_php_tags($str) + { + return str_replace(array(''), array('<?', '?>'), $str); + } + +} // End security diff --git a/includes/kohana/system/classes/kohana/session.php b/includes/kohana/system/classes/kohana/session.php new file mode 100644 index 00000000..8910a621 --- /dev/null +++ b/includes/kohana/system/classes/kohana/session.php @@ -0,0 +1,415 @@ +get($type); + + // Set the session class name + $class = 'Session_'.ucfirst($type); + + // Create a new session instance + Session::$instances[$type] = $session = new $class($config, $id); + + // Write the session at shutdown + register_shutdown_function(array($session, 'write')); + } + + return Session::$instances[$type]; + } + + // Cookie name + protected $_name = 'session'; + + // Cookie lifetime + protected $_lifetime = 0; + + // Encrypt session data? + protected $_encrypted = FALSE; + + // Session data + protected $_data = array(); + + // Is the session destroyed? + protected $_destroyed = FALSE; + + /** + * Overloads the name, lifetime, and encrypted session settings. + * + * [!!] Sessions can only be created using the [Session::instance] method. + * + * @param array configuration + * @param string session id + * @return void + * @uses Session::read + */ + public function __construct(array $config = NULL, $id = NULL) + { + if (isset($config['name'])) + { + // Cookie name to store the session id in + $this->_name = (string) $config['name']; + } + + if (isset($config['lifetime'])) + { + // Cookie lifetime + $this->_lifetime = (int) $config['lifetime']; + } + + if (isset($config['encrypted'])) + { + if ($config['encrypted'] === TRUE) + { + // Use the default Encrypt instance + $config['encrypted'] = 'default'; + } + + // Enable or disable encryption of data + $this->_encrypted = $config['encrypted']; + } + + // Load the session + $this->read($id); + } + + /** + * Session object is rendered to a serialized string. If encryption is + * enabled, the session will be encrypted. If not, the output string will + * be encoded using [base64_encode]. + * + * echo $session; + * + * @return string + * @uses Encrypt::encode + */ + public function __toString() + { + // Serialize the data array + $data = serialize($this->_data); + + if ($this->_encrypted) + { + // Encrypt the data using the default key + $data = Encrypt::instance($this->_encrypted)->encode($data); + } + else + { + // Obfuscate the data with base64 encoding + $data = base64_encode($data); + } + + return $data; + } + + /** + * Returns the current session array. The returned array can also be + * assigned by reference. + * + * // Get a copy of the current session data + * $data = $session->as_array(); + * + * // Assign by reference for modification + * $data =& $session->as_array(); + * + * @return array + */ + public function & as_array() + { + return $this->_data; + } + + /** + * Get the current session id, if the session supports it. + * + * $id = $session->id(); + * + * [!!] Not all session types have ids. + * + * @return string + * @since 3.0.8 + */ + public function id() + { + return NULL; + } + + /** + * Get the current session cookie name. + * + * $name = $session->id(); + * + * @return string + * @since 3.0.8 + */ + public function name() + { + return $this->_name; + } + + /** + * Get a variable from the session array. + * + * $foo = $session->get('foo'); + * + * @param string variable name + * @param mixed default value to return + * @return mixed + */ + public function get($key, $default = NULL) + { + return array_key_exists($key, $this->_data) ? $this->_data[$key] : $default; + } + + /** + * Get and delete a variable from the session array. + * + * $bar = $session->get_once('bar'); + * + * @param string variable name + * @param mixed default value to return + * @return mixed + */ + public function get_once($key, $default = NULL) + { + $value = $this->get($key, $default); + + unset($this->_data[$key]); + + return $value; + } + + /** + * Set a variable in the session array. + * + * $session->set('foo', 'bar'); + * + * @param string variable name + * @param mixed value + * @return $this + */ + public function set($key, $value) + { + $this->_data[$key] = $value; + + return $this; + } + + /** + * Set a variable by reference. + * + * $session->bind('foo', $foo); + * + * @param string variable name + * @param mixed referenced value + * @return $this + */ + public function bind($key, & $value) + { + $this->_data[$key] =& $value; + + return $this; + } + + /** + * Removes a variable in the session array. + * + * $session->delete('foo'); + * + * @param string variable name + * @param ... + * @return $this + */ + public function delete($key) + { + $args = func_get_args(); + + foreach ($args as $key) + { + unset($this->_data[$key]); + } + + return $this; + } + + /** + * Loads existing session data. + * + * $session->read(); + * + * @param string session id + * @return void + */ + public function read($id = NULL) + { + if (is_string($data = $this->_read($id))) + { + try + { + if ($this->_encrypted) + { + // Decrypt the data using the default key + $data = Encrypt::instance($this->_encrypted)->decode($data); + } + else + { + // Decode the base64 encoded data + $data = base64_decode($data); + } + + // Unserialize the data + $data = unserialize($data); + } + catch (Exception $e) + { + // Ignore all reading errors + } + } + + if (is_array($data)) + { + // Load the data locally + $this->_data = $data; + } + } + + /** + * Generates a new session id and returns it. + * + * $id = $session->regenerate(); + * + * @return string + */ + public function regenerate() + { + return $this->_regenerate(); + } + + /** + * Sets the last_active timestamp and saves the session. + * + * $session->write(); + * + * [!!] Any errors that occur during session writing will be logged, + * but not displayed, because sessions are written after output has + * been sent. + * + * @return boolean + * @uses Kohana::$log + */ + public function write() + { + if (headers_sent() OR $this->_destroyed) + { + // Session cannot be written when the headers are sent or when + // the session has been destroyed + return FALSE; + } + + // Set the last active timestamp + $this->_data['last_active'] = time(); + + try + { + return $this->_write(); + } + catch (Exception $e) + { + // Log & ignore all errors when a write fails + Kohana::$log->add(Kohana::ERROR, Kohana::exception_text($e))->write(); + + return FALSE; + } + } + + /** + * Completely destroy the current session. + * + * $success = $session->destroy(); + * + * @return boolean + */ + public function destroy() + { + if ($this->_destroyed === FALSE) + { + if ($this->_destroyed = $this->_destroy()) + { + // The session has been destroyed, clear all data + $this->_data = array(); + } + } + + return $this->_destroyed; + } + + /** + * Loads the raw session data string and returns it. + * + * @param string session id + * @return string + */ + abstract protected function _read($id = NULL); + + /** + * Generate a new session id and return it. + * + * @return string + */ + abstract protected function _regenerate(); + + /** + * Writes the current session. + * + * @return boolean + */ + abstract protected function _write(); + + /** + * Destroys the current session. + * + * @return boolean + */ + abstract protected function _destroy(); + +} // End Session diff --git a/includes/kohana/system/classes/kohana/session/cookie.php b/includes/kohana/system/classes/kohana/session/cookie.php new file mode 100644 index 00000000..9bf8c000 --- /dev/null +++ b/includes/kohana/system/classes/kohana/session/cookie.php @@ -0,0 +1,34 @@ +_name, NULL); + } + + protected function _regenerate() + { + // Cookie sessions have no id + return NULL; + } + + protected function _write() + { + return Cookie::set($this->_name, $this->__toString(), $this->_lifetime); + } + + protected function _destroy() + { + return Cookie::delete($this->_name); + } + +} // End Session_Cookie diff --git a/includes/kohana/system/classes/kohana/session/native.php b/includes/kohana/system/classes/kohana/session/native.php new file mode 100644 index 00000000..50342685 --- /dev/null +++ b/includes/kohana/system/classes/kohana/session/native.php @@ -0,0 +1,68 @@ +_lifetime, Cookie::$path, Cookie::$domain, Cookie::$secure, Cookie::$httponly); + + // Do not allow PHP to send Cache-Control headers + session_cache_limiter(FALSE); + + // Set the session cookie name + session_name($this->_name); + + if ($id) + { + // Set the session id + session_id($id); + } + + // Start the session + session_start(); + + // Use the $_SESSION global for storing data + $this->_data =& $_SESSION; + + return NULL; + } + + protected function _regenerate() + { + // Regenerate the session id + session_regenerate_id(); + + return session_id(); + } + + protected function _write() + { + // Write and close the session + session_write_close(); + + return TRUE; + } + + protected function _destroy() + { + // Destroy the current session + session_destroy(); + + return ! session_id(); + } + +} // End Session_Native diff --git a/includes/kohana/system/classes/kohana/text.php b/includes/kohana/system/classes/kohana/text.php new file mode 100644 index 00000000..813a62bb --- /dev/null +++ b/includes/kohana/system/classes/kohana/text.php @@ -0,0 +1,590 @@ + 'billion', + 1000000 => 'million', + 1000 => 'thousand', + 100 => 'hundred', + 90 => 'ninety', + 80 => 'eighty', + 70 => 'seventy', + 60 => 'sixty', + 50 => 'fifty', + 40 => 'fourty', + 30 => 'thirty', + 20 => 'twenty', + 19 => 'nineteen', + 18 => 'eighteen', + 17 => 'seventeen', + 16 => 'sixteen', + 15 => 'fifteen', + 14 => 'fourteen', + 13 => 'thirteen', + 12 => 'twelve', + 11 => 'eleven', + 10 => 'ten', + 9 => 'nine', + 8 => 'eight', + 7 => 'seven', + 6 => 'six', + 5 => 'five', + 4 => 'four', + 3 => 'three', + 2 => 'two', + 1 => 'one', + ); + + /** + * Limits a phrase to a given number of words. + * + * $text = Text::limit_words($text); + * + * @param string phrase to limit words of + * @param integer number of words to limit to + * @param string end character or entity + * @return string + */ + public static function limit_words($str, $limit = 100, $end_char = NULL) + { + $limit = (int) $limit; + $end_char = ($end_char === NULL) ? '…' : $end_char; + + if (trim($str) === '') + return $str; + + if ($limit <= 0) + return $end_char; + + preg_match('/^\s*+(?:\S++\s*+){1,'.$limit.'}/u', $str, $matches); + + // Only attach the end character if the matched string is shorter + // than the starting string. + return rtrim($matches[0]).(strlen($matches[0]) === strlen($str) ? '' : $end_char); + } + + /** + * Limits a phrase to a given number of characters. + * + * $text = Text::limit_chars($text); + * + * @param string phrase to limit characters of + * @param integer number of characters to limit to + * @param string end character or entity + * @param boolean enable or disable the preservation of words while limiting + * @return string + * @uses UTF8::strlen + */ + public static function limit_chars($str, $limit = 100, $end_char = NULL, $preserve_words = FALSE) + { + $end_char = ($end_char === NULL) ? '…' : $end_char; + + $limit = (int) $limit; + + if (trim($str) === '' OR UTF8::strlen($str) <= $limit) + return $str; + + if ($limit <= 0) + return $end_char; + + if ($preserve_words === FALSE) + return rtrim(UTF8::substr($str, 0, $limit)).$end_char; + + // Don't preserve words. The limit is considered the top limit. + // No strings with a length longer than $limit should be returned. + if ( ! preg_match('/^.{0,'.$limit.'}\s/us', $str, $matches)) + return $end_char; + + return rtrim($matches[0]).(strlen($matches[0]) === strlen($str) ? '' : $end_char); + } + + /** + * Alternates between two or more strings. + * + * echo Text::alternate('one', 'two'); // "one" + * echo Text::alternate('one', 'two'); // "two" + * echo Text::alternate('one', 'two'); // "one" + * + * Note that using multiple iterations of different strings may produce + * unexpected results. + * + * @param string strings to alternate between + * @return string + */ + public static function alternate() + { + static $i; + + if (func_num_args() === 0) + { + $i = 0; + return ''; + } + + $args = func_get_args(); + return $args[($i++ % count($args))]; + } + + /** + * Generates a random string of a given type and length. + * + * + * $str = Text::random(); // 8 character random string + * + * The following types are supported: + * + * alnum + * : Upper and lower case a-z, 0-9 (default) + * + * alpha + * : Upper and lower case a-z + * + * hexdec + * : Hexadecimal characters a-f, 0-9 + * + * distinct + * : Uppercase characters and numbers that cannot be confused + * + * You can also create a custom type by providing the "pool" of characters + * as the type. + * + * @param string a type of pool, or a string of characters to use as the pool + * @param integer length of string to return + * @return string + * @uses UTF8::split + */ + public static function random($type = NULL, $length = 8) + { + if ($type === NULL) + { + // Default is to generate an alphanumeric string + $type = 'alnum'; + } + + $utf8 = FALSE; + + switch ($type) + { + case 'alnum': + $pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + break; + case 'alpha': + $pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + break; + case 'hexdec': + $pool = '0123456789abcdef'; + break; + case 'numeric': + $pool = '0123456789'; + break; + case 'nozero': + $pool = '123456789'; + break; + case 'distinct': + $pool = '2345679ACDEFHJKLMNPRSTUVWXYZ'; + break; + default: + $pool = (string) $type; + $utf8 = ! UTF8::is_ascii($pool); + break; + } + + // Split the pool into an array of characters + $pool = ($utf8 === TRUE) ? UTF8::str_split($pool, 1) : str_split($pool, 1); + + // Largest pool key + $max = count($pool) - 1; + + $str = ''; + for ($i = 0; $i < $length; $i++) + { + // Select a random character from the pool and add it to the string + $str .= $pool[mt_rand(0, $max)]; + } + + // Make sure alnum strings contain at least one letter and one digit + if ($type === 'alnum' AND $length > 1) + { + if (ctype_alpha($str)) + { + // Add a random digit + $str[mt_rand(0, $length - 1)] = chr(mt_rand(48, 57)); + } + elseif (ctype_digit($str)) + { + // Add a random letter + $str[mt_rand(0, $length - 1)] = chr(mt_rand(65, 90)); + } + } + + return $str; + } + + /** + * Reduces multiple slashes in a string to single slashes. + * + * $str = Text::reduce_slashes('foo//bar/baz'); // "foo/bar/baz" + * + * @param string string to reduce slashes of + * @return string + */ + public static function reduce_slashes($str) + { + return preg_replace('#(? '#####', + * )); + * + * @param string phrase to replace words in + * @param array words to replace + * @param string replacement string + * @param boolean replace words across word boundries (space, period, etc) + * @return string + * @uses UTF8::strlen + */ + public static function censor($str, $badwords, $replacement = '#', $replace_partial_words = TRUE) + { + foreach ((array) $badwords as $key => $badword) + { + $badwords[$key] = str_replace('\*', '\S*?', preg_quote((string) $badword)); + } + + $regex = '('.implode('|', $badwords).')'; + + if ($replace_partial_words === FALSE) + { + // Just using \b isn't sufficient when we need to replace a badword that already contains word boundaries itself + $regex = '(?<=\b|\s|^)'.$regex.'(?=\b|\s|$)'; + } + + $regex = '!'.$regex.'!ui'; + + if (UTF8::strlen($replacement) == 1) + { + $regex .= 'e'; + return preg_replace($regex, 'str_repeat($replacement, UTF8::strlen(\'$1\'))', $str); + } + + return preg_replace($regex, $replacement, $str); + } + + /** + * Finds the text that is similar between a set of words. + * + * $match = Text::similar(array('fred', 'fran', 'free'); // "fr" + * + * @param array words to find similar text of + * @return string + */ + public static function similar(array $words) + { + // First word is the word to match against + $word = current($words); + + for ($i = 0, $max = strlen($word); $i < $max; ++$i) + { + foreach ($words as $w) + { + // Once a difference is found, break out of the loops + if ( ! isset($w[$i]) OR $w[$i] !== $word[$i]) + break 2; + } + } + + // Return the similar text + return substr($word, 0, $i); + } + + /** + * Converts text email addresses and anchors into links. Existing links + * will not be altered. + * + * echo Text::auto_link($text); + * + * [!!] This method is not foolproof since it uses regex to parse HTML. + * + * @param string text to auto link + * @return string + * @uses Text::auto_link_urls + * @uses Text::auto_link_emails + */ + public static function auto_link($text) + { + // Auto link emails first to prevent problems with "www.domain.com@example.com" + return Text::auto_link_urls(Text::auto_link_emails($text)); + } + + /** + * Converts text anchors into links. Existing links will not be altered. + * + * echo Text::auto_link_urls($text); + * + * [!!] This method is not foolproof since it uses regex to parse HTML. + * + * @param string text to auto link + * @return string + * @uses HTML::anchor + */ + public static function auto_link_urls($text) + { + // Find and replace all http/https/ftp/ftps links that are not part of an existing html anchor + $text = preg_replace_callback('~\b(?)(?:ht|f)tps?://\S+(?:/|\b)~i', 'Text::_auto_link_urls_callback1', $text); + + // Find and replace all naked www.links.com (without http://) + return preg_replace_callback('~\b(?)www(?:\.[a-z0-9][-a-z0-9]*+)+\.[a-z]{2,6}\b~i', 'Text::_auto_link_urls_callback2', $text); + } + + protected static function _auto_link_urls_callback1($matches) + { + return HTML::anchor($matches[0]); + } + + protected static function _auto_link_urls_callback2($matches) + { + return HTML::anchor('http://'.$matches[0], $matches[0]); + } + + /** + * Converts text email addresses into links. Existing links will not + * be altered. + * + * echo Text::auto_link_emails($text); + * + * [!!] This method is not foolproof since it uses regex to parse HTML. + * + * @param string text to auto link + * @return string + * @uses HTML::mailto + */ + public static function auto_link_emails($text) + { + // Find and replace all email addresses that are not part of an existing html mailto anchor + // Note: The "58;" negative lookbehind prevents matching of existing encoded html mailto anchors + // The html entity for a colon (:) is : or : or : etc. + return preg_replace_callback('~\b(?)~i', 'Text::_auto_link_emails_callback', $text); + } + + protected static function _auto_link_emails_callback($matches) + { + return HTML::mailto($matches[0]); + } + + /** + * Automatically applies "p" and "br" markup to text. + * Basically [nl2br](http://php.net/nl2br) on steroids. + * + * echo Text::auto_p($text); + * + * [!!] This method is not foolproof since it uses regex to parse HTML. + * + * @param string subject + * @param boolean convert single linebreaks to
            + * @return string + */ + public static function auto_p($str, $br = TRUE) + { + // Trim whitespace + if (($str = trim($str)) === '') + return ''; + + // Standardize newlines + $str = str_replace(array("\r\n", "\r"), "\n", $str); + + // Trim whitespace on each line + $str = preg_replace('~^[ \t]+~m', '', $str); + $str = preg_replace('~[ \t]+$~m', '', $str); + + // The following regexes only need to be executed if the string contains html + if ($html_found = (strpos($str, '<') !== FALSE)) + { + // Elements that should not be surrounded by p tags + $no_p = '(?:p|div|h[1-6r]|ul|ol|li|blockquote|d[dlt]|pre|t[dhr]|t(?:able|body|foot|head)|c(?:aption|olgroup)|form|s(?:elect|tyle)|a(?:ddress|rea)|ma(?:p|th))'; + + // Put at least two linebreaks before and after $no_p elements + $str = preg_replace('~^<'.$no_p.'[^>]*+>~im', "\n$0", $str); + $str = preg_replace('~$~im', "$0\n", $str); + } + + // Do the

            magic! + $str = '

            '.trim($str).'

            '; + $str = preg_replace('~\n{2,}~', "

            \n\n

            ", $str); + + // The following regexes only need to be executed if the string contains html + if ($html_found !== FALSE) + { + // Remove p tags around $no_p elements + $str = preg_replace('~

            (?=]*+>)~i', '', $str); + $str = preg_replace('~(]*+>)

            ~i', '$1', $str); + } + + // Convert single linebreaks to
            + if ($br === TRUE) + { + $str = preg_replace('~(?\n", $str); + } + + return $str; + } + + /** + * Returns human readable sizes. Based on original functions written by + * [Aidan Lister](http://aidanlister.com/repos/v/function.size_readable.php) + * and [Quentin Zervaas](http://www.phpriot.com/d/code/strings/filesize-format/). + * + * echo Text::bytes(filesize($file)); + * + * @param integer size in bytes + * @param string a definitive unit + * @param string the return string format + * @param boolean whether to use SI prefixes or IEC + * @return string + */ + public static function bytes($bytes, $force_unit = NULL, $format = NULL, $si = TRUE) + { + // Format string + $format = ($format === NULL) ? '%01.2f %s' : (string) $format; + + // IEC prefixes (binary) + if ($si == FALSE OR strpos($force_unit, 'i') !== FALSE) + { + $units = array('B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB'); + $mod = 1024; + } + // SI prefixes (decimal) + else + { + $units = array('B', 'kB', 'MB', 'GB', 'TB', 'PB'); + $mod = 1000; + } + + // Determine unit to use + if (($power = array_search((string) $force_unit, $units)) === FALSE) + { + $power = ($bytes > 0) ? floor(log($bytes, $mod)) : 0; + } + + return sprintf($format, $bytes / pow($mod, $power), $units[$power]); + } + + /** + * Format a number to human-readable text. + * + * // Display: one thousand and twenty-four + * echo Text::number(1024); + * + * // Display: five million, six hundred and thirty-two + * echo Text::number(5000632); + * + * @param integer number to format + * @return string + * @since 3.0.8 + */ + public static function number($number) + { + // The number must always be an integer + $number = (int) $number; + + // Uncompiled text version + $text = array(); + + // Last matched unit within the loop + $last_unit = NULL; + + // The last matched item within the loop + $last_item = ''; + + foreach (Text::$units as $unit => $name) + { + if ($number / $unit >= 1) + { + // $value = the number of times the number is divisble by unit + $number -= $unit * ($value = (int) floor($number / $unit)); + // Temporary var for textifying the current unit + $item = ''; + + if ($unit < 100) + { + if ($last_unit < 100 AND $last_unit >= 20) + { + $last_item .= '-'.$name; + } + else + { + $item = $name; + } + } + else + { + $item = Text::number($value).' '.$name; + } + + // In the situation that we need to make a composite number (i.e. twenty-three) + // then we need to modify the previous entry + if(empty($item)) + { + array_pop($text); + + $item = $last_item; + } + + $last_item = $text[] = $item; + $last_unit = $unit; + } + } + + if(count($text) > 1) + { + $and = array_pop($text); + } + + $text = implode(', ', $text); + + if(isset($and)) + { + $text .= ' and '.$and; + } + + return $text; + } + + /** + * Prevents [widow words](http://www.shauninman.com/archive/2006/08/22/widont_wordpress_plugin) + * by inserting a non-breaking space between the last two words. + * + * echo Text::widont($text); + * + * @param string text to remove widows from + * @return string + */ + public static function widont($str) + { + $str = rtrim($str); + $space = strrpos($str, ' '); + + if ($space !== FALSE) + { + $str = substr($str, 0, $space).' '.substr($str, $space + 1); + } + + return $str; + } + +} // End text diff --git a/includes/kohana/system/classes/kohana/upload.php b/includes/kohana/system/classes/kohana/upload.php new file mode 100644 index 00000000..bf1786cb --- /dev/null +++ b/includes/kohana/system/classes/kohana/upload.php @@ -0,0 +1,205 @@ +check()) + * { + * // Upload is valid, save it + * Upload::save($_FILES['file']); + * } + * + * @param array uploaded file data + * @param string new filename + * @param string new directory + * @param integer chmod mask + * @return string on success, full path to new file + * @return FALSE on failure + */ + public static function save(array $file, $filename = NULL, $directory = NULL, $chmod = 0644) + { + if ( ! isset($file['tmp_name']) OR ! is_uploaded_file($file['tmp_name'])) + { + // Ignore corrupted uploads + return FALSE; + } + + if ($filename === NULL) + { + // Use the default filename, with a timestamp pre-pended + $filename = uniqid().$file['name']; + } + + if (Upload::$remove_spaces === TRUE) + { + // Remove spaces from the filename + $filename = preg_replace('/\s+/', '_', $filename); + } + + if ($directory === NULL) + { + // Use the pre-configured upload directory + $directory = Upload::$default_directory; + } + + if ( ! is_dir($directory) OR ! is_writable(realpath($directory))) + { + throw new Kohana_Exception('Directory :dir must be writable', + array(':dir' => Kohana::debug_path($directory))); + } + + // Make the filename into a complete path + $filename = realpath($directory).DIRECTORY_SEPARATOR.$filename; + + if (move_uploaded_file($file['tmp_name'], $filename)) + { + if ($chmod !== FALSE) + { + // Set permissions on filename + chmod($filename, $chmod); + } + + // Return new file path + return $filename; + } + + return FALSE; + } + + /** + * Tests if upload data is valid, even if no file was uploaded. If you + * _do_ require a file to be uploaded, add the [Upload::not_empty] rule + * before this rule. + * + * $array->rule('file', 'Upload::valid') + * + * @param array $_FILES item + * @return bool + */ + public static function valid($file) + { + return (isset($file['error']) + AND isset($file['name']) + AND isset($file['type']) + AND isset($file['tmp_name']) + AND isset($file['size'])); + } + + /** + * Tests if a successful upload has been made. + * + * $array->rule('file', 'Upload::not_empty'); + * + * @param array $_FILES item + * @return bool + */ + public static function not_empty(array $file) + { + return (isset($file['error']) + AND isset($file['tmp_name']) + AND $file['error'] === UPLOAD_ERR_OK + AND is_uploaded_file($file['tmp_name']) + ); + } + + /** + * Test if an uploaded file is an allowed file type, by extension. + * + * $array->rule('file', 'Upload::type', array(array('jpg', 'png', 'gif'))); + * + * @param array $_FILES item + * @param array allowed file extensions + * @return bool + */ + public static function type(array $file, array $allowed) + { + if ($file['error'] !== UPLOAD_ERR_OK) + return TRUE; + + $ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + + return in_array($ext, $allowed); + } + + /** + * Validation rule to test if an uploaded file is allowed by file size. + * File sizes are defined as: SB, where S is the size (1, 15, 300, etc) and + * B is the byte modifier: (B)ytes, (K)ilobytes, (M)egabytes, (G)igabytes. + * + * $array->rule('file', 'Upload::size', array('1M')) + * + * @param array $_FILES item + * @param string maximum file size + * @return bool + */ + public static function size(array $file, $size) + { + if ($file['error'] === UPLOAD_ERR_INI_SIZE) + { + // Upload is larger than PHP allowed size + return FALSE; + } + + if ($file['error'] !== UPLOAD_ERR_OK) + { + // The upload failed, no size to check + return TRUE; + } + + // Only one size is allowed + $size = strtoupper(trim($size)); + + if ( ! preg_match('/^[0-9]++[BKMG]$/', $size)) + { + throw new Kohana_Exception('Size does not contain a digit and a byte value: :size', array( + ':size' => $size, + )); + } + + // Make the size into a power of 1024 + switch (substr($size, -1)) + { + case 'G': $size = intval($size) * pow(1024, 3); break; + case 'M': $size = intval($size) * pow(1024, 2); break; + case 'K': $size = intval($size) * pow(1024, 1); break; + default: $size = intval($size); break; + } + + // Test that the file is under or equal to the max size + return ($file['size'] <= $size); + } + +} // End upload diff --git a/includes/kohana/system/classes/kohana/url.php b/includes/kohana/system/classes/kohana/url.php new file mode 100644 index 00000000..b7f4352f --- /dev/null +++ b/includes/kohana/system/classes/kohana/url.php @@ -0,0 +1,175 @@ + 'title', 'limit' => 10)); + * + * Typically you would use this when you are sorting query results, + * or something similar. + * + * [!!] Parameters with a NULL value are left out. + * + * @param array array of GET parameters + * @return string + */ + public static function query(array $params = NULL) + { + if ($params === NULL) + { + // Use only the current parameters + $params = $_GET; + } + else + { + // Merge the current and new parameters + $params = array_merge($_GET, $params); + } + + if (empty($params)) + { + // No query parameters + return ''; + } + + $query = http_build_query($params, '', '&'); + + // Don't prepend '?' to an empty string + return ($query === '') ? '' : '?'.$query; + } + + /** + * Convert a phrase to a URL-safe title. + * + * echo URL::title('My Blog Post'); // "my-blog-post" + * + * @param string phrase to convert + * @param string word separator (any single character) + * @param boolean transliterate to ASCII? + * @return string + * @uses UTF8::transliterate_to_ascii + */ + public static function title($title, $separator = '-', $ascii_only = FALSE) + { + if ($ascii_only === TRUE) + { + // Transliterate non-ASCII characters + $title = UTF8::transliterate_to_ascii($title); + + // Remove all characters that are not the separator, a-z, 0-9, or whitespace + $title = preg_replace('![^'.preg_quote($separator).'a-z0-9\s]+!', '', strtolower($title)); + } + else + { + // Remove all characters that are not the separator, letters, numbers, or whitespace + $title = preg_replace('![^'.preg_quote($separator).'\pL\pN\s]+!u', '', UTF8::strtolower($title)); + } + + // Replace all separator characters and whitespace by a single separator + $title = preg_replace('!['.preg_quote($separator).'\s]+!u', $separator, $title); + + // Trim separators from the beginning and end + return trim($title, $separator); + } + +} // End url \ No newline at end of file diff --git a/includes/kohana/system/classes/kohana/utf8.php b/includes/kohana/system/classes/kohana/utf8.php new file mode 100644 index 00000000..1f68aa86 --- /dev/null +++ b/includes/kohana/system/classes/kohana/utf8.php @@ -0,0 +1,761 @@ + $val) + { + // Recursion! + $var[self::clean($key)] = self::clean($val); + } + } + elseif (is_string($var) AND $var !== '') + { + // Remove control characters + $var = self::strip_ascii_ctrl($var); + + if ( ! self::is_ascii($var)) + { + // Disable notices + $ER = error_reporting(~E_NOTICE); + + // iconv is expensive, so it is only used when needed + $var = iconv($charset, $charset.'//IGNORE', $var); + + // Turn notices back on + error_reporting($ER); + } + } + + return $var; + } + + /** + * Tests whether a string contains only 7-bit ASCII bytes. This is used to + * determine when to use native functions or UTF-8 functions. + * + * $ascii = UTF8::is_ascii($str); + * + * @param mixed string or array of strings to check + * @return boolean + */ + public static function is_ascii($str) + { + if (is_array($str)) + { + $str = implode($str); + } + + return ! preg_match('/[^\x00-\x7F]/S', $str); + } + + /** + * Strips out device control codes in the ASCII range. + * + * $str = UTF8::strip_ascii_ctrl($str); + * + * @param string string to clean + * @return string + */ + public static function strip_ascii_ctrl($str) + { + return preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]+/S', '', $str); + } + + /** + * Strips out all non-7bit ASCII bytes. + * + * $str = UTF8::strip_non_ascii($str); + * + * @param string string to clean + * @return string + */ + public static function strip_non_ascii($str) + { + return preg_replace('/[^\x00-\x7F]+/S', '', $str); + } + + /** + * Replaces special/accented UTF-8 characters by ASCII-7 "equivalents". + * + * $ascii = UTF8::transliterate_to_ascii($utf8); + * + * @author Andreas Gohr + * @param string string to transliterate + * @param integer -1 lowercase only, +1 uppercase only, 0 both cases + * @return string + */ + public static function transliterate_to_ascii($str, $case = 0) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _transliterate_to_ascii($str, $case); + } + + /** + * Returns the length of the given string. This is a UTF8-aware version + * of [strlen](http://php.net/strlen). + * + * $length = UTF8::strlen($str); + * + * @param string string being measured for length + * @return integer + * @uses UTF8::$server_utf8 + */ + public static function strlen($str) + { + if (UTF8::$server_utf8) + return mb_strlen($str, Kohana::$charset); + + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _strlen($str); + } + + /** + * Finds position of first occurrence of a UTF-8 string. This is a + * UTF8-aware version of [strpos](http://php.net/strpos). + * + * $position = UTF8::strpos($str, $search); + * + * @author Harry Fuecks + * @param string haystack + * @param string needle + * @param integer offset from which character in haystack to start searching + * @return integer position of needle + * @return boolean FALSE if the needle is not found + * @uses UTF8::$server_utf8 + */ + public static function strpos($str, $search, $offset = 0) + { + if (UTF8::$server_utf8) + return mb_strpos($str, $search, $offset, Kohana::$charset); + + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _strpos($str, $search, $offset); + } + + /** + * Finds position of last occurrence of a char in a UTF-8 string. This is + * a UTF8-aware version of [strrpos](http://php.net/strrpos). + * + * $position = UTF8::strrpos($str, $search); + * + * @author Harry Fuecks + * @param string haystack + * @param string needle + * @param integer offset from which character in haystack to start searching + * @return integer position of needle + * @return boolean FALSE if the needle is not found + * @uses UTF8::$server_utf8 + */ + public static function strrpos($str, $search, $offset = 0) + { + if (UTF8::$server_utf8) + return mb_strrpos($str, $search, $offset, Kohana::$charset); + + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _strrpos($str, $search, $offset); + } + + /** + * Returns part of a UTF-8 string. This is a UTF8-aware version + * of [substr](http://php.net/substr). + * + * $sub = UTF8::substr($str, $offset); + * + * @author Chris Smith + * @param string input string + * @param integer offset + * @param integer length limit + * @return string + * @uses UTF8::$server_utf8 + * @uses Kohana::$charset + */ + public static function substr($str, $offset, $length = NULL) + { + if (UTF8::$server_utf8) + return ($length === NULL) + ? mb_substr($str, $offset, mb_strlen($str), Kohana::$charset) + : mb_substr($str, $offset, $length, Kohana::$charset); + + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _substr($str, $offset, $length); + } + + /** + * Replaces text within a portion of a UTF-8 string. This is a UTF8-aware + * version of [substr_replace](http://php.net/substr_replace). + * + * $str = UTF8::substr_replace($str, $replacement, $offset); + * + * @author Harry Fuecks + * @param string input string + * @param string replacement string + * @param integer offset + * @return string + */ + public static function substr_replace($str, $replacement, $offset, $length = NULL) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _substr_replace($str, $replacement, $offset, $length); + } + + /** + * Makes a UTF-8 string lowercase. This is a UTF8-aware version + * of [strtolower](http://php.net/strtolower). + * + * $str = UTF8::strtolower($str); + * + * @author Andreas Gohr + * @param string mixed case string + * @return string + * @uses UTF8::$server_utf8 + */ + public static function strtolower($str) + { + if (UTF8::$server_utf8) + return mb_strtolower($str, Kohana::$charset); + + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _strtolower($str); + } + + /** + * Makes a UTF-8 string uppercase. This is a UTF8-aware version + * of [strtoupper](http://php.net/strtoupper). + * + * @author Andreas Gohr + * @param string mixed case string + * @return string + * @uses UTF8::$server_utf8 + * @uses Kohana::$charset + */ + public static function strtoupper($str) + { + if (UTF8::$server_utf8) + return mb_strtoupper($str, Kohana::$charset); + + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _strtoupper($str); + } + + /** + * Makes a UTF-8 string's first character uppercase. This is a UTF8-aware + * version of [ucfirst](http://php.net/ucfirst). + * + * $str = UTF8::ucfirst($str); + * + * @author Harry Fuecks + * @param string mixed case string + * @return string + */ + public static function ucfirst($str) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _ucfirst($str); + } + + /** + * Makes the first character of every word in a UTF-8 string uppercase. + * This is a UTF8-aware version of [ucwords](http://php.net/ucwords). + * + * $str = UTF8::ucwords($str); + * + * @author Harry Fuecks + * @param string mixed case string + * @return string + * @uses UTF8::$server_utf8 + */ + public static function ucwords($str) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _ucwords($str); + } + + /** + * Case-insensitive UTF-8 string comparison. This is a UTF8-aware version + * of [strcasecmp](http://php.net/strcasecmp). + * + * $compare = UTF8::strcasecmp($str1, $str2); + * + * @author Harry Fuecks + * @param string string to compare + * @param string string to compare + * @return integer less than 0 if str1 is less than str2 + * @return integer greater than 0 if str1 is greater than str2 + * @return integer 0 if they are equal + */ + public static function strcasecmp($str1, $str2) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _strcasecmp($str1, $str2); + } + + /** + * Returns a string or an array with all occurrences of search in subject + * (ignoring case) and replaced with the given replace value. This is a + * UTF8-aware version of [str_ireplace](http://php.net/str_ireplace). + * + * [!!] This function is very slow compared to the native version. Avoid + * using it when possible. + * + * @author Harry Fuecks + * @param string input string + * @param string needle + * @return string matched substring if found + * @return FALSE if the substring was not found + */ + public static function stristr($str, $search) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _stristr($str, $search); + } + + /** + * Finds the length of the initial segment matching mask. This is a + * UTF8-aware version of [strspn](http://php.net/strspn). + * + * $found = UTF8::strspn($str, $mask); + * + * @author Harry Fuecks + * @param string input string + * @param string mask for search + * @param integer start position of the string to examine + * @param integer length of the string to examine + * @return integer length of the initial segment that contains characters in the mask + */ + public static function strspn($str, $mask, $offset = NULL, $length = NULL) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _strspn($str, $mask, $offset, $length); + } + + /** + * Finds the length of the initial segment not matching mask. This is a + * UTF8-aware version of [strcspn](http://php.net/strcspn). + * + * $found = UTF8::strcspn($str, $mask); + * + * @author Harry Fuecks + * @param string input string + * @param string mask for search + * @param integer start position of the string to examine + * @param integer length of the string to examine + * @return integer length of the initial segment that contains characters not in the mask + */ + public static function strcspn($str, $mask, $offset = NULL, $length = NULL) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _strcspn($str, $mask, $offset, $length); + } + + /** + * Pads a UTF-8 string to a certain length with another string. This is a + * UTF8-aware version of [str_pad](http://php.net/str_pad). + * + * $str = UTF8::str_pad($str, $length); + * + * @author Harry Fuecks + * @param string input string + * @param integer desired string length after padding + * @param string string to use as padding + * @param string padding type: STR_PAD_RIGHT, STR_PAD_LEFT, or STR_PAD_BOTH + * @return string + */ + public static function str_pad($str, $final_str_length, $pad_str = ' ', $pad_type = STR_PAD_RIGHT) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _str_pad($str, $final_str_length, $pad_str, $pad_type); + } + + /** + * Converts a UTF-8 string to an array. This is a UTF8-aware version of + * [str_split](http://php.net/str_split). + * + * $array = UTF8::str_split($str); + * + * @author Harry Fuecks + * @param string input string + * @param integer maximum length of each chunk + * @return array + */ + public static function str_split($str, $split_length = 1) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _str_split($str, $split_length); + } + + /** + * Reverses a UTF-8 string. This is a UTF8-aware version of [strrev](http://php.net/strrev). + * + * $str = UTF8::strrev($str); + * + * @author Harry Fuecks + * @param string string to be reversed + * @return string + */ + public static function strrev($str) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _strrev($str); + } + + /** + * Strips whitespace (or other UTF-8 characters) from the beginning and + * end of a string. This is a UTF8-aware version of [trim](http://php.net/trim). + * + * $str = UTF8::trim($str); + * + * @author Andreas Gohr + * @param string input string + * @param string string of characters to remove + * @return string + */ + public static function trim($str, $charlist = NULL) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _trim($str, $charlist); + } + + /** + * Strips whitespace (or other UTF-8 characters) from the beginning of + * a string. This is a UTF8-aware version of [ltrim](http://php.net/ltrim). + * + * $str = UTF8::ltrim($str); + * + * @author Andreas Gohr + * @param string input string + * @param string string of characters to remove + * @return string + */ + public static function ltrim($str, $charlist = NULL) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _ltrim($str, $charlist); + } + + /** + * Strips whitespace (or other UTF-8 characters) from the end of a string. + * This is a UTF8-aware version of [rtrim](http://php.net/rtrim). + * + * $str = UTF8::rtrim($str); + * + * @author Andreas Gohr + * @param string input string + * @param string string of characters to remove + * @return string + */ + public static function rtrim($str, $charlist = NULL) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _rtrim($str, $charlist); + } + + /** + * Returns the unicode ordinal for a character. This is a UTF8-aware + * version of [ord](http://php.net/ord). + * + * $digit = UTF8::ord($character); + * + * @author Harry Fuecks + * @param string UTF-8 encoded character + * @return integer + */ + public static function ord($chr) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _ord($chr); + } + + /** + * Takes an UTF-8 string and returns an array of ints representing the Unicode characters. + * Astral planes are supported i.e. the ints in the output can be > 0xFFFF. + * Occurrences of the BOM are ignored. Surrogates are not allowed. + * + * $array = UTF8::to_unicode($str); + * + * The Original Code is Mozilla Communicator client code. + * The Initial Developer of the Original Code is Netscape Communications Corporation. + * Portions created by the Initial Developer are Copyright (C) 1998 the Initial Developer. + * Ported to PHP by Henri Sivonen , see + * Slight modifications to fit with phputf8 library by Harry Fuecks + * + * @param string UTF-8 encoded string + * @return array unicode code points + * @return FALSE if the string is invalid + */ + public static function to_unicode($str) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _to_unicode($str); + } + + /** + * Takes an array of ints representing the Unicode characters and returns a UTF-8 string. + * Astral planes are supported i.e. the ints in the input can be > 0xFFFF. + * Occurrances of the BOM are ignored. Surrogates are not allowed. + * + * $str = UTF8::to_unicode($array); + * + * The Original Code is Mozilla Communicator client code. + * The Initial Developer of the Original Code is Netscape Communications Corporation. + * Portions created by the Initial Developer are Copyright (C) 1998 the Initial Developer. + * Ported to PHP by Henri Sivonen , see http://hsivonen.iki.fi/php-utf8/ + * Slight modifications to fit with phputf8 library by Harry Fuecks . + * + * @param array unicode code points representing a string + * @return string utf8 string of characters + * @return boolean FALSE if a code point cannot be found + */ + public static function from_unicode($arr) + { + if ( ! isset(self::$called[__FUNCTION__])) + { + require SYSPATH.'utf8'.DIRECTORY_SEPARATOR.__FUNCTION__.EXT; + + // Function has been called + self::$called[__FUNCTION__] = TRUE; + } + + return _from_unicode($arr); + } + +} // End UTF8 + +if (Kohana_UTF8::$server_utf8 === NULL) +{ + // Determine if this server supports UTF-8 natively + Kohana_UTF8::$server_utf8 = extension_loaded('mbstring'); +} diff --git a/includes/kohana/system/classes/kohana/validate.php b/includes/kohana/system/classes/kohana/validate.php new file mode 100644 index 00000000..376dfdc9 --- /dev/null +++ b/includes/kohana/system/classes/kohana/validate.php @@ -0,0 +1,1161 @@ +getArrayCopy(); + } + + // Value cannot be NULL, FALSE, '', or an empty array + return ! in_array($value, array(NULL, FALSE, '', array()), TRUE); + } + + /** + * Checks a field against a regular expression. + * + * @param string value + * @param string regular expression to match (including delimiters) + * @return boolean + */ + public static function regex($value, $expression) + { + return (bool) preg_match($expression, (string) $value); + } + + /** + * Checks that a field is long enough. + * + * @param string value + * @param integer minimum length required + * @return boolean + */ + public static function min_length($value, $length) + { + return UTF8::strlen($value) >= $length; + } + + /** + * Checks that a field is short enough. + * + * @param string value + * @param integer maximum length required + * @return boolean + */ + public static function max_length($value, $length) + { + return UTF8::strlen($value) <= $length; + } + + /** + * Checks that a field is exactly the right length. + * + * @param string value + * @param integer exact length required + * @return boolean + */ + public static function exact_length($value, $length) + { + return UTF8::strlen($value) === $length; + } + + /** + * Check an email address for correct format. + * + * @link http://www.iamcal.com/publish/articles/php/parsing_email/ + * @link http://www.w3.org/Protocols/rfc822/ + * + * @param string email address + * @param boolean strict RFC compatibility + * @return boolean + */ + public static function email($email, $strict = FALSE) + { + if ($strict === TRUE) + { + $qtext = '[^\\x0d\\x22\\x5c\\x80-\\xff]'; + $dtext = '[^\\x0d\\x5b-\\x5d\\x80-\\xff]'; + $atom = '[^\\x00-\\x20\\x22\\x28\\x29\\x2c\\x2e\\x3a-\\x3c\\x3e\\x40\\x5b-\\x5d\\x7f-\\xff]+'; + $pair = '\\x5c[\\x00-\\x7f]'; + + $domain_literal = "\\x5b($dtext|$pair)*\\x5d"; + $quoted_string = "\\x22($qtext|$pair)*\\x22"; + $sub_domain = "($atom|$domain_literal)"; + $word = "($atom|$quoted_string)"; + $domain = "$sub_domain(\\x2e$sub_domain)*"; + $local_part = "$word(\\x2e$word)*"; + + $expression = "/^$local_part\\x40$domain$/D"; + } + else + { + $expression = '/^[-_a-z0-9\'+*$^&%=~!?{}]++(?:\.[-_a-z0-9\'+*$^&%=~!?{}]+)*+@(?:(?![-.])[-a-z0-9.]+(? 253) + return FALSE; + + // An extra check for the top level domain + // It must start with a letter + $tld = ltrim(substr($matches[1], (int) strrpos($matches[1], '.')), '.'); + return ctype_alpha($tld[0]); + } + + /** + * Validate an IP. + * + * @param string IP address + * @param boolean allow private IP networks + * @return boolean + */ + public static function ip($ip, $allow_private = TRUE) + { + // Do not allow reserved addresses + $flags = FILTER_FLAG_NO_RES_RANGE; + + if ($allow_private === FALSE) + { + // Do not allow private or reserved addresses + $flags = $flags | FILTER_FLAG_NO_PRIV_RANGE; + } + + return (bool) filter_var($ip, FILTER_VALIDATE_IP, $flags); + } + + /** + * Validates a credit card number, with a Luhn check if possible. + * + * @param integer credit card number + * @param string|array card type, or an array of card types + * @return boolean + * @uses Validate::luhn + */ + public static function credit_card($number, $type = NULL) + { + // Remove all non-digit characters from the number + if (($number = preg_replace('/\D+/', '', $number)) === '') + return FALSE; + + if ($type == NULL) + { + // Use the default type + $type = 'default'; + } + elseif (is_array($type)) + { + foreach ($type as $t) + { + // Test each type for validity + if (Validate::credit_card($number, $t)) + return TRUE; + } + + return FALSE; + } + + $cards = Kohana::config('credit_cards'); + + // Check card type + $type = strtolower($type); + + if ( ! isset($cards[$type])) + return FALSE; + + // Check card number length + $length = strlen($number); + + // Validate the card length by the card type + if ( ! in_array($length, preg_split('/\D+/', $cards[$type]['length']))) + return FALSE; + + // Check card number prefix + if ( ! preg_match('/^'.$cards[$type]['prefix'].'/', $number)) + return FALSE; + + // No Luhn check required + if ($cards[$type]['luhn'] == FALSE) + return TRUE; + + return Validate::luhn($number); + } + + /** + * Validate a number against the [Luhn](http://en.wikipedia.org/wiki/Luhn_algorithm) + * (mod10) formula. + * + * @param string number to check + * @return boolean + */ + public static function luhn($number) + { + // Force the value to be a string as this method uses string functions. + // Converting to an integer may pass PHP_INT_MAX and result in an error! + $number = (string) $number; + + if ( ! ctype_digit($number)) + { + // Luhn can only be used on numbers! + return FALSE; + } + + // Check number length + $length = strlen($number); + + // Checksum of the card number + $checksum = 0; + + for ($i = $length - 1; $i >= 0; $i -= 2) + { + // Add up every 2nd digit, starting from the right + $checksum += substr($number, $i, 1); + } + + for ($i = $length - 2; $i >= 0; $i -= 2) + { + // Add up every 2nd digit doubled, starting from the right + $double = substr($number, $i, 1) * 2; + + // Subtract 9 from the double where value is greater than 10 + $checksum += ($double >= 10) ? $double - 9 : $double; + } + + // If the checksum is a multiple of 10, the number is valid + return ($checksum % 10 === 0); + } + + /** + * Checks if a phone number is valid. + * + * @param string phone number to check + * @return boolean + */ + public static function phone($number, $lengths = NULL) + { + if ( ! is_array($lengths)) + { + $lengths = array(7,10,11); + } + + // Remove all non-digit characters from the number + $number = preg_replace('/\D+/', '', $number); + + // Check if the number is within range + return in_array(strlen($number), $lengths); + } + + /** + * Tests if a string is a valid date string. + * + * @param string date to check + * @return boolean + */ + public static function date($str) + { + return (strtotime($str) !== FALSE); + } + + /** + * Checks whether a string consists of alphabetical characters only. + * + * @param string input string + * @param boolean trigger UTF-8 compatibility + * @return boolean + */ + public static function alpha($str, $utf8 = FALSE) + { + $str = (string) $str; + + if ($utf8 === TRUE) + { + return (bool) preg_match('/^\pL++$/uD', $str); + } + else + { + return ctype_alpha($str); + } + } + + /** + * Checks whether a string consists of alphabetical characters and numbers only. + * + * @param string input string + * @param boolean trigger UTF-8 compatibility + * @return boolean + */ + public static function alpha_numeric($str, $utf8 = FALSE) + { + if ($utf8 === TRUE) + { + return (bool) preg_match('/^[\pL\pN]++$/uD', $str); + } + else + { + return ctype_alnum($str); + } + } + + /** + * Checks whether a string consists of alphabetical characters, numbers, underscores and dashes only. + * + * @param string input string + * @param boolean trigger UTF-8 compatibility + * @return boolean + */ + public static function alpha_dash($str, $utf8 = FALSE) + { + if ($utf8 === TRUE) + { + $regex = '/^[-\pL\pN_]++$/uD'; + } + else + { + $regex = '/^[-a-z0-9_]++$/iD'; + } + + return (bool) preg_match($regex, $str); + } + + /** + * Checks whether a string consists of digits only (no dots or dashes). + * + * @param string input string + * @param boolean trigger UTF-8 compatibility + * @return boolean + */ + public static function digit($str, $utf8 = FALSE) + { + if ($utf8 === TRUE) + { + return (bool) preg_match('/^\pN++$/uD', $str); + } + else + { + return (is_int($str) AND $str >= 0) OR ctype_digit($str); + } + } + + /** + * Checks whether a string is a valid number (negative and decimal numbers allowed). + * + * Uses {@link http://www.php.net/manual/en/function.localeconv.php locale conversion} + * to allow decimal point to be locale specific. + * + * @param string input string + * @return boolean + */ + public static function numeric($str) + { + // Get the decimal point for the current locale + list($decimal) = array_values(localeconv()); + + // A lookahead is used to make sure the string contains at least one digit (before or after the decimal point) + return (bool) preg_match('/^-?+(?=.*[0-9])[0-9]*+'.preg_quote($decimal).'?+[0-9]*+$/D', (string) $str); + } + + /** + * Tests if a number is within a range. + * + * @param string number to check + * @param integer minimum value + * @param integer maximum value + * @return boolean + */ + public static function range($number, $min, $max) + { + return ($number >= $min AND $number <= $max); + } + + /** + * Checks if a string is a proper decimal format. Optionally, a specific + * number of digits can be checked too. + * + * @param string number to check + * @param integer number of decimal places + * @param integer number of digits + * @return boolean + */ + public static function decimal($str, $places = 2, $digits = NULL) + { + if ($digits > 0) + { + // Specific number of digits + $digits = '{'.(int) $digits.'}'; + } + else + { + // Any number of digits + $digits = '+'; + } + + // Get the decimal point for the current locale + list($decimal) = array_values(localeconv()); + + return (bool) preg_match('/^[0-9]'.$digits.preg_quote($decimal).'[0-9]{'.(int) $places.'}$/D', $str); + } + + /** + * Checks if a string is a proper hexadecimal HTML color value. The validation + * is quite flexible as it does not require an initial "#" and also allows for + * the short notation using only three instead of six hexadecimal characters. + * + * @param string input string + * @return boolean + */ + public static function color($str) + { + return (bool) preg_match('/^#?+[0-9a-f]{3}(?:[0-9a-f]{3})?$/iD', $str); + } + + // Field filters + protected $_filters = array(); + + // Field rules + protected $_rules = array(); + + // Field callbacks + protected $_callbacks = array(); + + // Field labels + protected $_labels = array(); + + // Rules that are executed even when the value is empty + protected $_empty_rules = array('not_empty', 'matches'); + + // Error list, field => rule + protected $_errors = array(); + + /** + * Sets the unique "any field" key and creates an ArrayObject from the + * passed array. + * + * @param array array to validate + * @return void + */ + public function __construct(array $array) + { + parent::__construct($array, ArrayObject::STD_PROP_LIST); + } + + /** + * Copies the current filter/rule/callback to a new array. + * + * $copy = $array->copy($new_data); + * + * @param array new data set + * @return Validation + * @since 3.0.5 + */ + public function copy(array $array) + { + // Create a copy of the current validation set + $copy = clone $this; + + // Replace the data set + $copy->exchangeArray($array); + + return $copy; + } + + /** + * Returns the array representation of the current object. + * + * @return array + */ + public function as_array() + { + return $this->getArrayCopy(); + } + + /** + * Sets or overwrites the label name for a field. + * + * @param string field name + * @param string label + * @return $this + */ + public function label($field, $label) + { + // Set the label for this field + $this->_labels[$field] = $label; + + return $this; + } + + /** + * Sets labels using an array. + * + * @param array list of field => label names + * @return $this + */ + public function labels(array $labels) + { + $this->_labels = $labels + $this->_labels; + + return $this; + } + + /** + * Overwrites or appends filters to a field. Each filter will be executed once. + * All rules must be valid PHP callbacks. + * + * // Run trim() on all fields + * $validation->filter(TRUE, 'trim'); + * + * @param string field name + * @param mixed valid PHP callback + * @param array extra parameters for the filter + * @return $this + */ + public function filter($field, $filter, array $params = NULL) + { + if ($field !== TRUE AND ! isset($this->_labels[$field])) + { + // Set the field label to the field name + $this->_labels[$field] = preg_replace('/[^\pL]+/u', ' ', $field); + } + + // Store the filter and params for this rule + $this->_filters[$field][$filter] = (array) $params; + + return $this; + } + + /** + * Add filters using an array. + * + * @param string field name + * @param array list of functions or static method name + * @return $this + */ + public function filters($field, array $filters) + { + foreach ($filters as $filter => $params) + { + $this->filter($field, $filter, $params); + } + + return $this; + } + + /** + * Overwrites or appends rules to a field. Each rule will be executed once. + * All rules must be string names of functions method names. + * + * // The "username" must not be empty and have a minimum length of 4 + * $validation->rule('username', 'not_empty') + * ->rule('username', 'min_length', array(4)); + * + * @param string field name + * @param string function or static method name + * @param array extra parameters for the rule + * @return $this + */ + public function rule($field, $rule, array $params = NULL) + { + if ($field !== TRUE AND ! isset($this->_labels[$field])) + { + // Set the field label to the field name + $this->_labels[$field] = preg_replace('/[^\pL]+/u', ' ', $field); + } + + if('matches' === $rule AND ! isset($this->_labels[$params[0]])) + { + $match_field = $params[0]; + $this->_labels[$match_field] = preg_replace('/[^\pL]+/u', ' ', $match_field); + } + + // Store the rule and params for this rule + $this->_rules[$field][$rule] = (array) $params; + + return $this; + } + + /** + * Add rules using an array. + * + * @param string field name + * @param array list of functions or static method name + * @return $this + */ + public function rules($field, array $rules) + { + foreach ($rules as $rule => $params) + { + $this->rule($field, $rule, $params); + } + + return $this; + } + + /** + * Adds a callback to a field. Each callback will be executed only once. + * + * // The "username" must be checked with a custom method + * $validation->callback('username', array($this, 'check_username')); + * + * To add a callback to every field already set, use TRUE for the field name. + * + * @param string field name + * @param mixed callback to add + * @param array extra parameters for the callback + * @return $this + */ + public function callback($field, $callback, array $params = array()) + { + if ( ! isset($this->_callbacks[$field])) + { + // Create the list for this field + $this->_callbacks[$field] = array(); + } + + if ($field !== TRUE AND ! isset($this->_labels[$field])) + { + // Set the field label to the field name + $this->_labels[$field] = preg_replace('/[^\pL]+/u', ' ', $field); + } + + if ( ! in_array($callback, $this->_callbacks[$field], TRUE)) + { + // Store the callback + $this->_callbacks[$field][] = array($callback, $params); + } + + return $this; + } + + /** + * Add callbacks using an array. + * + * @param string field name + * @param array list of callbacks + * @return $this + */ + public function callbacks($field, array $callbacks) + { + foreach ($callbacks as $callback) + { + $this->callback($field, $callback); + } + + return $this; + } + + /** + * Executes all validation filters, rules, and callbacks. This should + * typically be called within an if/else block. + * + * if ($validation->check()) + * { + * // The data is valid, do something here + * } + * + * @param boolean allow empty array? + * @return boolean + */ + public function check($allow_empty = FALSE) + { + if (Kohana::$profiling === TRUE) + { + // Start a new benchmark + $benchmark = Profiler::start('Validation', __FUNCTION__); + } + + // New data set + $data = $this->_errors = array(); + + // Assume nothing has been submitted + $submitted = FALSE; + + // Get a list of the expected fields + $expected = array_keys($this->_labels); + + // Import the filters, rules, and callbacks locally + $filters = $this->_filters; + $rules = $this->_rules; + $callbacks = $this->_callbacks; + + foreach ($expected as $field) + { + if (isset($this[$field])) + { + // Some data has been submitted, continue validation + $submitted = TRUE; + + // Use the submitted value + $data[$field] = $this[$field]; + } + else + { + // No data exists for this field + $data[$field] = NULL; + } + + if (isset($filters[TRUE])) + { + if ( ! isset($filters[$field])) + { + // Initialize the filters for this field + $filters[$field] = array(); + } + + // Append the filters + $filters[$field] += $filters[TRUE]; + } + + if (isset($rules[TRUE])) + { + if ( ! isset($rules[$field])) + { + // Initialize the rules for this field + $rules[$field] = array(); + } + + // Append the rules + $rules[$field] += $rules[TRUE]; + } + + if (isset($callbacks[TRUE])) + { + if ( ! isset($callbacks[$field])) + { + // Initialize the callbacks for this field + $callbacks[$field] = array(); + } + + // Append the callbacks + $callbacks[$field] += $callbacks[TRUE]; + } + } + + // Overload the current array with the new one + $this->exchangeArray($data); + + if ($submitted === FALSE AND ! $allow_empty) + { + // Because no data was submitted, validation will not be forced + return FALSE; + } + + // Remove the filters, rules, and callbacks that apply to every field + unset($filters[TRUE], $rules[TRUE], $callbacks[TRUE]); + + // Execute the filters + + foreach ($filters as $field => $set) + { + // Get the field value + $value = $this[$field]; + + foreach ($set as $filter => $params) + { + // Add the field value to the parameters + array_unshift($params, $value); + + if (strpos($filter, '::') === FALSE) + { + // Use a function call + $function = new ReflectionFunction($filter); + + // Call $function($this[$field], $param, ...) with Reflection + $value = $function->invokeArgs($params); + } + else + { + // Split the class and method of the rule + list($class, $method) = explode('::', $filter, 2); + + // Use a static method call + $method = new ReflectionMethod($class, $method); + + // Call $Class::$method($this[$field], $param, ...) with Reflection + $value = $method->invokeArgs(NULL, $params); + } + } + + // Set the filtered value + $this[$field] = $value; + } + + // Execute the rules + + foreach ($rules as $field => $set) + { + // Get the field value + $value = $this[$field]; + + foreach ($set as $rule => $params) + { + if ( ! in_array($rule, $this->_empty_rules) AND ! Validate::not_empty($value)) + { + // Skip this rule for empty fields + continue; + } + + // Add the field value to the parameters + array_unshift($params, $value); + + if (method_exists($this, $rule)) + { + // Use a method in this object + $method = new ReflectionMethod($this, $rule); + + if ($method->isStatic()) + { + // Call static::$rule($this[$field], $param, ...) with Reflection + $passed = $method->invokeArgs(NULL, $params); + } + else + { + // Do not use Reflection here, the method may be protected + $passed = call_user_func_array(array($this, $rule), $params); + } + } + elseif (strpos($rule, '::') === FALSE) + { + // Use a function call + $function = new ReflectionFunction($rule); + + // Call $function($this[$field], $param, ...) with Reflection + $passed = $function->invokeArgs($params); + } + else + { + // Split the class and method of the rule + list($class, $method) = explode('::', $rule, 2); + + // Use a static method call + $method = new ReflectionMethod($class, $method); + + // Call $Class::$method($this[$field], $param, ...) with Reflection + $passed = $method->invokeArgs(NULL, $params); + } + + if ($passed === FALSE) + { + // Remove the field value from the parameters + array_shift($params); + + // Add the rule to the errors + $this->error($field, $rule, $params); + + // This field has an error, stop executing rules + break; + } + } + } + + // Execute the callbacks + + foreach ($callbacks as $field => $set) + { + if (isset($this->_errors[$field])) + { + // Skip any field that already has an error + continue; + } + + foreach ($set as $callback_array) + { + list($callback, $params) = $callback_array; + + if (is_string($callback) AND strpos($callback, '::') !== FALSE) + { + // Make the static callback into an array + $callback = explode('::', $callback, 2); + } + + if (is_array($callback)) + { + // Separate the object and method + list ($object, $method) = $callback; + + // Use a method in the given object + $method = new ReflectionMethod($object, $method); + + if ( ! is_object($object)) + { + // The object must be NULL for static calls + $object = NULL; + } + + // Call $object->$method($this, $field, $errors) with Reflection + $method->invoke($object, $this, $field, $params); + } + else + { + // Use a function call + $function = new ReflectionFunction($callback); + + // Call $function($this, $field, $errors) with Reflection + $function->invoke($this, $field, $params); + } + + if (isset($this->_errors[$field])) + { + // An error was added, stop processing callbacks + break; + } + } + } + + if (isset($benchmark)) + { + // Stop benchmarking + Profiler::stop($benchmark); + } + + return empty($this->_errors); + } + + /** + * Add an error to a field. + * + * @param string field name + * @param string error message + * @return $this + */ + public function error($field, $error, array $params = NULL) + { + $this->_errors[$field] = array($error, $params); + + return $this; + } + + /** + * Returns the error messages. If no file is specified, the error message + * will be the name of the rule that failed. When a file is specified, the + * message will be loaded from "field/rule", or if no rule-specific message + * exists, "field/default" will be used. If neither is set, the returned + * message will be "file/field/rule". + * + * By default all messages are translated using the default language. + * A string can be used as the second parameter to specified the language + * that the message was written in. + * + * // Get errors from messages/forms/login.php + * $errors = $validate->errors('forms/login'); + * + * @uses Kohana::message + * @param string file to load error messages from + * @param mixed translate the message + * @return array + */ + public function errors($file = NULL, $translate = TRUE) + { + if ($file === NULL) + { + // Return the error list + return $this->_errors; + } + + // Create a new message list + $messages = array(); + + foreach ($this->_errors as $field => $set) + { + list($error, $params) = $set; + + // Get the label for this field + $label = $this->_labels[$field]; + + if ($translate) + { + // Translate the label + $label = __($label); + } + + // Start the translation values list + $values = array( + ':field' => $label, + ':value' => $this[$field], + ); + + if (is_array($values[':value'])) + { + // All values must be strings + $values[':value'] = implode(', ', Arr::flatten($values[':value'])); + } + + if ($params) + { + foreach ($params as $key => $value) + { + if (is_array($value)) + { + // All values must be strings + $value = implode(', ', Arr::flatten($value)); + } + + // Check if a label for this parameter exists + if (isset($this->_labels[$value])) + { + $value = $this->_labels[$value]; + + if ($translate) + { + // Translate the label + $value = __($value); + } + } + + // Add each parameter as a numbered value, starting from 1 + $values[':param'.($key + 1)] = $value; + } + } + + if ($message = Kohana::message($file, "{$field}.{$error}")) + { + // Found a message for this field and error + } + elseif ($message = Kohana::message($file, "{$field}.default")) + { + // Found a default message for this field + } + elseif ($message = Kohana::message($file, $error)) + { + // Found a default message for this error + } + elseif ($message = Kohana::message('validate', $error)) + { + // Found a default message for this error + } + else + { + // No message exists, display the path expected + $message = "{$file}.{$field}.{$error}"; + } + + if ($translate) + { + if (is_string($translate)) + { + // Translate the message using specified language + $message = __($message, $values, $translate); + } + else + { + // Translate the message using the default language + $message = __($message, $values); + } + } + else + { + // Do not translate, just replace the values + $message = strtr($message, $values); + } + + // Set the message for this field + $messages[$field] = $message; + } + + return $messages; + } + + /** + * Checks if a field matches the value of another field. + * + * @param string field value + * @param string field name to match + * @return boolean + */ + protected function matches($value, $match) + { + return ($value === $this[$match]); + } + +} // End Validation diff --git a/includes/kohana/system/classes/kohana/validate/exception.php b/includes/kohana/system/classes/kohana/validate/exception.php new file mode 100644 index 00000000..9b0a7275 --- /dev/null +++ b/includes/kohana/system/classes/kohana/validate/exception.php @@ -0,0 +1,23 @@ +array = $array; + + parent::__construct($message, $values, $code); + } + +} // End Kohana_Validate_Exception diff --git a/includes/kohana/system/classes/kohana/view.php b/includes/kohana/system/classes/kohana/view.php new file mode 100644 index 00000000..5fae7178 --- /dev/null +++ b/includes/kohana/system/classes/kohana/view.php @@ -0,0 +1,346 @@ + $value) + { + View::$_global_data[$key2] = $value; + } + } + else + { + View::$_global_data[$key] = $value; + } + } + + /** + * Assigns a global variable by reference, similar to [View::bind], except + * that the variable will be accessible to all views. + * + * View::bind_global($key, $value); + * + * @param string variable name + * @param mixed referenced variable + * @return void + */ + public static function bind_global($key, & $value) + { + View::$_global_data[$key] =& $value; + } + + // View filename + protected $_file; + + // Array of local variables + protected $_data = array(); + + /** + * Sets the initial view filename and local data. Views should almost + * always only be created using [View::factory]. + * + * $view = new View($file); + * + * @param string view filename + * @param array array of values + * @return void + * @uses View::set_filename + */ + public function __construct($file = NULL, array $data = NULL) + { + if ($file !== NULL) + { + $this->set_filename($file); + } + + if ( $data !== NULL) + { + // Add the values to the current data + $this->_data = $data + $this->_data; + } + } + + /** + * Magic method, searches for the given variable and returns its value. + * Local variables will be returned before global variables. + * + * $value = $view->foo; + * + * [!!] If the variable has not yet been set, an exception will be thrown. + * + * @param string variable name + * @return mixed + * @throws Kohana_Exception + */ + public function & __get($key) + { + if (isset($this->_data[$key])) + { + return $this->_data[$key]; + } + elseif (isset(View::$_global_data[$key])) + { + return View::$_global_data[$key]; + } + else + { + throw new Kohana_Exception('View variable is not set: :var', + array(':var' => $key)); + } + } + + /** + * Magic method, calls [View::set] with the same parameters. + * + * $view->foo = 'something'; + * + * @param string variable name + * @param mixed value + * @return void + */ + public function __set($key, $value) + { + $this->set($key, $value); + } + + /** + * Magic method, determines if a variable is set. + * + * isset($view->foo); + * + * [!!] `NULL` variables are not considered to be set by [isset](http://php.net/isset). + * + * @param string variable name + * @return boolean + */ + public function __isset($key) + { + return (isset($this->_data[$key]) OR isset(View::$_global_data[$key])); + } + + /** + * Magic method, unsets a given variable. + * + * unset($view->foo); + * + * @param string variable name + * @return void + */ + public function __unset($key) + { + unset($this->_data[$key], View::$_global_data[$key]); + } + + /** + * Magic method, returns the output of [View::render]. + * + * @return string + * @uses View::render + */ + public function __toString() + { + try + { + return $this->render(); + } + catch (Exception $e) + { + // Display the exception message + Kohana::exception_handler($e); + + return ''; + } + } + + /** + * Sets the view filename. + * + * $view->set_filename($file); + * + * @param string view filename + * @return View + * @throws Kohana_View_Exception + */ + public function set_filename($file) + { + if (($path = Kohana::find_file('views', $file)) === FALSE) + { + throw new Kohana_View_Exception('The requested view :file could not be found', array( + ':file' => $file, + )); + } + + // Store the file path locally + $this->_file = $path; + + return $this; + } + + /** + * Assigns a variable by name. Assigned values will be available as a + * variable within the view file: + * + * // This value can be accessed as $foo within the view + * $view->set('foo', 'my value'); + * + * You can also use an array to set several values at once: + * + * // Create the values $food and $beverage in the view + * $view->set(array('food' => 'bread', 'beverage' => 'water')); + * + * @param string variable name or an array of variables + * @param mixed value + * @return $this + */ + public function set($key, $value = NULL) + { + if (is_array($key)) + { + foreach ($key as $name => $value) + { + $this->_data[$name] = $value; + } + } + else + { + $this->_data[$key] = $value; + } + + return $this; + } + + /** + * Assigns a value by reference. The benefit of binding is that values can + * be altered without re-setting them. It is also possible to bind variables + * before they have values. Assigned values will be available as a + * variable within the view file: + * + * // This reference can be accessed as $ref within the view + * $view->bind('ref', $bar); + * + * @param string variable name + * @param mixed referenced variable + * @return $this + */ + public function bind($key, & $value) + { + $this->_data[$key] =& $value; + + return $this; + } + + /** + * Renders the view object to a string. Global and local data are merged + * and extracted to create local variables within the view file. + * + * $output = $view->render(); + * + * [!!] Global variables with the same key name as local variables will be + * overwritten by the local variable. + * + * @param string view filename + * @return string + * @throws Kohana_View_Exception + * @uses View::capture + */ + public function render($file = NULL) + { + if ($file !== NULL) + { + $this->set_filename($file); + } + + if (empty($this->_file)) + { + throw new Kohana_View_Exception('You must set the file to use within your view before rendering'); + } + + // Combine local and global data and capture the output + return View::capture($this->_file, $this->_data); + } + +} // End View diff --git a/includes/kohana/system/classes/kohana/view/exception.php b/includes/kohana/system/classes/kohana/view/exception.php new file mode 100644 index 00000000..f8752db3 --- /dev/null +++ b/includes/kohana/system/classes/kohana/view/exception.php @@ -0,0 +1,9 @@ + array( + 'length' => '13,14,15,16,17,18,19', + 'prefix' => '', + 'luhn' => TRUE, + ), + + 'american express' => array( + 'length' => '15', + 'prefix' => '3[47]', + 'luhn' => TRUE, + ), + + 'diners club' => array( + 'length' => '14,16', + 'prefix' => '36|55|30[0-5]', + 'luhn' => TRUE, + ), + + 'discover' => array( + 'length' => '16', + 'prefix' => '6(?:5|011)', + 'luhn' => TRUE, + ), + + 'jcb' => array( + 'length' => '15,16', + 'prefix' => '3|1800|2131', + 'luhn' => TRUE, + ), + + 'maestro' => array( + 'length' => '16,18', + 'prefix' => '50(?:20|38)|6(?:304|759)', + 'luhn' => TRUE, + ), + + 'mastercard' => array( + 'length' => '16', + 'prefix' => '5[1-5]', + 'luhn' => TRUE, + ), + + 'visa' => array( + 'length' => '13,16', + 'prefix' => '4', + 'luhn' => TRUE, + ), + +); \ No newline at end of file diff --git a/includes/kohana/system/config/encrypt.php b/includes/kohana/system/config/encrypt.php new file mode 100644 index 00000000..ae9126a6 --- /dev/null +++ b/includes/kohana/system/config/encrypt.php @@ -0,0 +1,17 @@ + array( + /** + * The following options must be set: + * + * string key secret passphrase + * integer mode encryption mode, one of MCRYPT_MODE_* + * integer cipher encryption cipher, one of the Mcrpyt cipher constants + */ + 'cipher' => MCRYPT_RIJNDAEL_128, + 'mode' => MCRYPT_MODE_NOFB, + ), + +); diff --git a/includes/kohana/system/config/inflector.php b/includes/kohana/system/config/inflector.php new file mode 100644 index 00000000..d66ec6ed --- /dev/null +++ b/includes/kohana/system/config/inflector.php @@ -0,0 +1,61 @@ + array( + 'access', + 'advice', + 'art', + 'baggage', + 'dances', + 'equipment', + 'fish', + 'fuel', + 'furniture', + 'heat', + 'honey', + 'homework', + 'impatience', + 'information', + 'knowledge', + 'luggage', + 'money', + 'music', + 'news', + 'patience', + 'progress', + 'pollution', + 'research', + 'rice', + 'sand', + 'series', + 'sheep', + 'sms', + 'species', + 'staff', + 'toothpaste', + 'traffic', + 'understanding', + 'water', + 'weather', + 'work', + ), + + 'irregular' => array( + 'child' => 'children', + 'clothes' => 'clothing', + 'man' => 'men', + 'movie' => 'movies', + 'person' => 'people', + 'woman' => 'women', + 'mouse' => 'mice', + 'goose' => 'geese', + 'ox' => 'oxen', + 'leaf' => 'leaves', + 'course' => 'courses', + 'size' => 'sizes', + 'was' => 'were', + 'is' => 'are', + 'verse' => 'verses', + ), +); diff --git a/includes/kohana/system/config/mimes.php b/includes/kohana/system/config/mimes.php new file mode 100644 index 00000000..e63a9ca5 --- /dev/null +++ b/includes/kohana/system/config/mimes.php @@ -0,0 +1,225 @@ + array('text/h323'), + '7z' => array('application/x-7z-compressed'), + 'abw' => array('application/x-abiword'), + 'acx' => array('application/internet-property-stream'), + 'ai' => array('application/postscript'), + 'aif' => array('audio/x-aiff'), + 'aifc' => array('audio/x-aiff'), + 'aiff' => array('audio/x-aiff'), + 'asf' => array('video/x-ms-asf'), + 'asr' => array('video/x-ms-asf'), + 'asx' => array('video/x-ms-asf'), + 'atom' => array('application/atom+xml'), + 'avi' => array('video/avi', 'video/msvideo', 'video/x-msvideo'), + 'bin' => array('application/octet-stream','application/macbinary'), + 'bmp' => array('image/bmp'), + 'c' => array('text/x-csrc'), + 'c++' => array('text/x-c++src'), + 'cab' => array('application/x-cab'), + 'cc' => array('text/x-c++src'), + 'cda' => array('application/x-cdf'), + 'class' => array('application/octet-stream'), + 'cpp' => array('text/x-c++src'), + 'cpt' => array('application/mac-compactpro'), + 'csh' => array('text/x-csh'), + 'css' => array('text/css'), + 'csv' => array('text/x-comma-separated-values', 'application/vnd.ms-excel', 'text/comma-separated-values', 'text/csv'), + 'dbk' => array('application/docbook+xml'), + 'dcr' => array('application/x-director'), + 'deb' => array('application/x-debian-package'), + 'diff' => array('text/x-diff'), + 'dir' => array('application/x-director'), + 'divx' => array('video/divx'), + 'dll' => array('application/octet-stream', 'application/x-msdos-program'), + 'dmg' => array('application/x-apple-diskimage'), + 'dms' => array('application/octet-stream'), + 'doc' => array('application/msword'), + 'docx' => array('application/vnd.openxmlformats-officedocument.wordprocessingml.document'), + 'dvi' => array('application/x-dvi'), + 'dxr' => array('application/x-director'), + 'eml' => array('message/rfc822'), + 'eps' => array('application/postscript'), + 'evy' => array('application/envoy'), + 'exe' => array('application/x-msdos-program', 'application/octet-stream'), + 'fla' => array('application/octet-stream'), + 'flac' => array('application/x-flac'), + 'flc' => array('video/flc'), + 'fli' => array('video/fli'), + 'flv' => array('video/x-flv'), + 'gif' => array('image/gif'), + 'gtar' => array('application/x-gtar'), + 'gz' => array('application/x-gzip'), + 'h' => array('text/x-chdr'), + 'h++' => array('text/x-c++hdr'), + 'hh' => array('text/x-c++hdr'), + 'hpp' => array('text/x-c++hdr'), + 'hqx' => array('application/mac-binhex40'), + 'hs' => array('text/x-haskell'), + 'htm' => array('text/html'), + 'html' => array('text/html'), + 'ico' => array('image/x-icon'), + 'ics' => array('text/calendar'), + 'iii' => array('application/x-iphone'), + 'ins' => array('application/x-internet-signup'), + 'iso' => array('application/x-iso9660-image'), + 'isp' => array('application/x-internet-signup'), + 'jar' => array('application/java-archive'), + 'java' => array('application/x-java-applet'), + 'jpe' => array('image/jpeg', 'image/pjpeg'), + 'jpeg' => array('image/jpeg', 'image/pjpeg'), + 'jpg' => array('image/jpeg', 'image/pjpeg'), + 'js' => array('application/x-javascript'), + 'json' => array('application/json'), + 'latex' => array('application/x-latex'), + 'lha' => array('application/octet-stream'), + 'log' => array('text/plain', 'text/x-log'), + 'lzh' => array('application/octet-stream'), + 'm4a' => array('audio/mpeg'), + 'm4p' => array('video/mp4v-es'), + 'm4v' => array('video/mp4'), + 'man' => array('application/x-troff-man'), + 'mdb' => array('application/x-msaccess'), + 'midi' => array('audio/midi'), + 'mid' => array('audio/midi'), + 'mif' => array('application/vnd.mif'), + 'mka' => array('audio/x-matroska'), + 'mkv' => array('video/x-matroska'), + 'mov' => array('video/quicktime'), + 'movie' => array('video/x-sgi-movie'), + 'mp2' => array('audio/mpeg'), + 'mp3' => array('audio/mpeg'), + 'mp4' => array('application/mp4','audio/mp4','video/mp4'), + 'mpa' => array('video/mpeg'), + 'mpe' => array('video/mpeg'), + 'mpeg' => array('video/mpeg'), + 'mpg' => array('video/mpeg'), + 'mpg4' => array('video/mp4'), + 'mpga' => array('audio/mpeg'), + 'mpp' => array('application/vnd.ms-project'), + 'mpv' => array('video/x-matroska'), + 'mpv2' => array('video/mpeg'), + 'ms' => array('application/x-troff-ms'), + 'msg' => array('application/msoutlook','application/x-msg'), + 'msi' => array('application/x-msi'), + 'nws' => array('message/rfc822'), + 'oda' => array('application/oda'), + 'odb' => array('application/vnd.oasis.opendocument.database'), + 'odc' => array('application/vnd.oasis.opendocument.chart'), + 'odf' => array('application/vnd.oasis.opendocument.forumla'), + 'odg' => array('application/vnd.oasis.opendocument.graphics'), + 'odi' => array('application/vnd.oasis.opendocument.image'), + 'odm' => array('application/vnd.oasis.opendocument.text-master'), + 'odp' => array('application/vnd.oasis.opendocument.presentation'), + 'ods' => array('application/vnd.oasis.opendocument.spreadsheet'), + 'odt' => array('application/vnd.oasis.opendocument.text'), + 'oga' => array('audio/ogg'), + 'ogg' => array('application/ogg'), + 'ogv' => array('video/ogg'), + 'otg' => array('application/vnd.oasis.opendocument.graphics-template'), + 'oth' => array('application/vnd.oasis.opendocument.web'), + 'otp' => array('application/vnd.oasis.opendocument.presentation-template'), + 'ots' => array('application/vnd.oasis.opendocument.spreadsheet-template'), + 'ott' => array('application/vnd.oasis.opendocument.template'), + 'p' => array('text/x-pascal'), + 'pas' => array('text/x-pascal'), + 'patch' => array('text/x-diff'), + 'pbm' => array('image/x-portable-bitmap'), + 'pdf' => array('application/pdf', 'application/x-download'), + 'php' => array('application/x-httpd-php'), + 'php3' => array('application/x-httpd-php'), + 'php4' => array('application/x-httpd-php'), + 'php5' => array('application/x-httpd-php'), + 'phps' => array('application/x-httpd-php-source'), + 'phtml' => array('application/x-httpd-php'), + 'pl' => array('text/x-perl'), + 'pm' => array('text/x-perl'), + 'png' => array('image/png', 'image/x-png'), + 'po' => array('text/x-gettext-translation'), + 'pot' => array('application/vnd.ms-powerpoint'), + 'pps' => array('application/vnd.ms-powerpoint'), + 'ppt' => array('application/powerpoint'), + 'pptx' => array('application/vnd.openxmlformats-officedocument.presentationml.presentation'), + 'ps' => array('application/postscript'), + 'psd' => array('application/x-photoshop', 'image/x-photoshop'), + 'pub' => array('application/x-mspublisher'), + 'py' => array('text/x-python'), + 'qt' => array('video/quicktime'), + 'ra' => array('audio/x-realaudio'), + 'ram' => array('audio/x-realaudio', 'audio/x-pn-realaudio'), + 'rar' => array('application/rar'), + 'rgb' => array('image/x-rgb'), + 'rm' => array('audio/x-pn-realaudio'), + 'rpm' => array('audio/x-pn-realaudio-plugin', 'application/x-redhat-package-manager'), + 'rss' => array('application/rss+xml'), + 'rtf' => array('text/rtf'), + 'rtx' => array('text/richtext'), + 'rv' => array('video/vnd.rn-realvideo'), + 'sea' => array('application/octet-stream'), + 'sh' => array('text/x-sh'), + 'shtml' => array('text/html'), + 'sit' => array('application/x-stuffit'), + 'smi' => array('application/smil'), + 'smil' => array('application/smil'), + 'so' => array('application/octet-stream'), + 'src' => array('application/x-wais-source'), + 'svg' => array('image/svg+xml'), + 'swf' => array('application/x-shockwave-flash'), + 't' => array('application/x-troff'), + 'tar' => array('application/x-tar'), + 'tcl' => array('text/x-tcl'), + 'tex' => array('application/x-tex'), + 'text' => array('text/plain'), + 'texti' => array('application/x-texinfo'), + 'textinfo' => array('application/x-texinfo'), + 'tgz' => array('application/x-tar'), + 'tif' => array('image/tiff'), + 'tiff' => array('image/tiff'), + 'torrent' => array('application/x-bittorrent'), + 'tr' => array('application/x-troff'), + 'tsv' => array('text/tab-separated-values'), + 'txt' => array('text/plain'), + 'wav' => array('audio/x-wav'), + 'wax' => array('audio/x-ms-wax'), + 'wbxml' => array('application/wbxml'), + 'wm' => array('video/x-ms-wm'), + 'wma' => array('audio/x-ms-wma'), + 'wmd' => array('application/x-ms-wmd'), + 'wmlc' => array('application/wmlc'), + 'wmv' => array('video/x-ms-wmv', 'application/octet-stream'), + 'wmx' => array('video/x-ms-wmx'), + 'wmz' => array('application/x-ms-wmz'), + 'word' => array('application/msword', 'application/octet-stream'), + 'wp5' => array('application/wordperfect5.1'), + 'wpd' => array('application/vnd.wordperfect'), + 'wvx' => array('video/x-ms-wvx'), + 'xbm' => array('image/x-xbitmap'), + 'xcf' => array('image/xcf'), + 'xhtml' => array('application/xhtml+xml'), + 'xht' => array('application/xhtml+xml'), + 'xl' => array('application/excel', 'application/vnd.ms-excel'), + 'xla' => array('application/excel', 'application/vnd.ms-excel'), + 'xlc' => array('application/excel', 'application/vnd.ms-excel'), + 'xlm' => array('application/excel', 'application/vnd.ms-excel'), + 'xls' => array('application/excel', 'application/vnd.ms-excel'), + 'xlsx' => array('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'), + 'xlt' => array('application/excel', 'application/vnd.ms-excel'), + 'xml' => array('text/xml', 'application/xml'), + 'xof' => array('x-world/x-vrml'), + 'xpm' => array('image/x-xpixmap'), + 'xsl' => array('text/xml'), + 'xvid' => array('video/x-xvid'), + 'xwd' => array('image/x-xwindowdump'), + 'z' => array('application/x-compress'), + 'zip' => array('application/x-zip', 'application/zip', 'application/x-zip-compressed') +); diff --git a/includes/kohana/system/config/session.php b/includes/kohana/system/config/session.php new file mode 100644 index 00000000..78ac9fa4 --- /dev/null +++ b/includes/kohana/system/config/session.php @@ -0,0 +1,7 @@ + array( + 'encrypted' => FALSE, + ), +); diff --git a/includes/kohana/system/config/user_agents.php b/includes/kohana/system/config/user_agents.php new file mode 100644 index 00000000..5753c232 --- /dev/null +++ b/includes/kohana/system/config/user_agents.php @@ -0,0 +1,104 @@ + array( + 'windows nt 6.1' => 'Windows 7', + 'windows nt 6.0' => 'Windows Vista', + 'windows nt 5.2' => 'Windows 2003', + 'windows nt 5.1' => 'Windows XP', + 'windows nt 5.0' => 'Windows 2000', + 'windows nt 4.0' => 'Windows NT', + 'winnt4.0' => 'Windows NT', + 'winnt 4.0' => 'Windows NT', + 'winnt' => 'Windows NT', + 'windows 98' => 'Windows 98', + 'win98' => 'Windows 98', + 'windows 95' => 'Windows 95', + 'win95' => 'Windows 95', + 'windows' => 'Unknown Windows OS', + 'os x' => 'Mac OS X', + 'intel mac' => 'Intel Mac', + 'ppc mac' => 'PowerPC Mac', + 'powerpc' => 'PowerPC', + 'ppc' => 'PowerPC', + 'cygwin' => 'Cygwin', + 'linux' => 'Linux', + 'debian' => 'Debian', + 'openvms' => 'OpenVMS', + 'sunos' => 'Sun Solaris', + 'amiga' => 'Amiga', + 'beos' => 'BeOS', + 'apachebench' => 'ApacheBench', + 'freebsd' => 'FreeBSD', + 'netbsd' => 'NetBSD', + 'bsdi' => 'BSDi', + 'openbsd' => 'OpenBSD', + 'os/2' => 'OS/2', + 'warp' => 'OS/2', + 'aix' => 'AIX', + 'irix' => 'Irix', + 'osf' => 'DEC OSF', + 'hp-ux' => 'HP-UX', + 'hurd' => 'GNU/Hurd', + 'unix' => 'Unknown Unix OS', + ), + + 'browser' => array( + 'Opera' => 'Opera', + 'MSIE' => 'Internet Explorer', + 'Internet Explorer' => 'Internet Explorer', + 'Shiira' => 'Shiira', + 'Firefox' => 'Firefox', + 'Chimera' => 'Chimera', + 'Phoenix' => 'Phoenix', + 'Firebird' => 'Firebird', + 'Camino' => 'Camino', + 'Navigator' => 'Netscape', + 'Netscape' => 'Netscape', + 'OmniWeb' => 'OmniWeb', + 'Chrome' => 'Chrome', + 'Safari' => 'Safari', + 'CFNetwork' => 'Safari', // Core Foundation for OSX, WebKit/Safari + 'Konqueror' => 'Konqueror', + 'Epiphany' => 'Epiphany', + 'Galeon' => 'Galeon', + 'Mozilla' => 'Mozilla', + 'icab' => 'iCab', + 'lynx' => 'Lynx', + 'links' => 'Links', + 'hotjava' => 'HotJava', + 'amaya' => 'Amaya', + 'IBrowse' => 'IBrowse', + ), + + 'mobile' => array( + 'mobileexplorer' => 'Mobile Explorer', + 'openwave' => 'Open Wave', + 'opera mini' => 'Opera Mini', + 'operamini' => 'Opera Mini', + 'elaine' => 'Palm', + 'palmsource' => 'Palm', + 'digital paths' => 'Palm', + 'avantgo' => 'Avantgo', + 'xiino' => 'Xiino', + 'palmscape' => 'Palmscape', + 'nokia' => 'Nokia', + 'ericsson' => 'Ericsson', + 'blackBerry' => 'BlackBerry', + 'motorola' => 'Motorola', + 'iphone' => 'iPhone', + 'android' => 'Android', + ), + + 'robot' => array( + 'googlebot' => 'Googlebot', + 'msnbot' => 'MSNBot', + 'slurp' => 'Inktomi Slurp', + 'yahoo' => 'Yahoo', + 'askjeeves' => 'AskJeeves', + 'fastcrawler' => 'FastCrawler', + 'infoseek' => 'InfoSeek Robot 1.0', + 'lycos' => 'Lycos', + ), +); diff --git a/includes/kohana/system/i18n/en.php b/includes/kohana/system/i18n/en.php new file mode 100644 index 00000000..16276f1c --- /dev/null +++ b/includes/kohana/system/i18n/en.php @@ -0,0 +1,3 @@ + 'Español', + 'Hello, world!' => '¡Hola, mundo!', +); \ No newline at end of file diff --git a/includes/kohana/system/i18n/fr.php b/includes/kohana/system/i18n/fr.php new file mode 100644 index 00000000..b8ed57fd --- /dev/null +++ b/includes/kohana/system/i18n/fr.php @@ -0,0 +1,7 @@ + 'Français', + 'Hello, world!' => 'Bonjour, monde!', +); \ No newline at end of file diff --git a/includes/kohana/system/messages/validate.php b/includes/kohana/system/messages/validate.php new file mode 100644 index 00000000..50d330c7 --- /dev/null +++ b/includes/kohana/system/messages/validate.php @@ -0,0 +1,25 @@ + ':field must contain only letters', + 'alpha_dash' => ':field must contain only letters and dashes', + 'alpha_numeric' => ':field must contain only letters and numbers', + 'color' => ':field must be a color', + 'credit_card' => ':field must be a credit card number', + 'date' => ':field must be a date', + 'decimal' => ':field must be a decimal with :param1 places', + 'digit' => ':field must be a digit', + 'email' => ':field must be a email address', + 'email_domain' => ':field must contain a valid email domain', + 'exact_length' => ':field must be exactly :param1 characters long', + 'in_array' => ':field must be one of the available options', + 'ip' => ':field must be an ip address', + 'matches' => ':field must be the same as :param1', + 'min_length' => ':field must be at least :param1 characters long', + 'max_length' => ':field must be less than :param1 characters long', + 'phone' => ':field must be a phone number', + 'not_empty' => ':field must not be empty', + 'range' => ':field must be within the range of :param1 to :param2', + 'regex' => ':field does not match the required format', + 'url' => ':field must be a url', +); \ No newline at end of file diff --git a/includes/kohana/system/tests/kohana/ArrTest.php b/includes/kohana/system/tests/kohana/ArrTest.php new file mode 100644 index 00000000..ce7789d4 --- /dev/null +++ b/includes/kohana/system/tests/kohana/ArrTest.php @@ -0,0 +1,461 @@ + + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +Class Kohana_ArrTest extends Kohana_Unittest_TestCase +{ + /** + * Provides test data for test_callback() + * + * @return array + */ + public function provider_callback() + { + return array( + // Tests.... + // That no parameters returns null + array('function', array('function', NULL)), + // That we can get an array of parameters values + array('function(1,2,3)', array('function', array('1', '2', '3'))), + // That it's not just using the callback "function" + array('different_name(harry,jerry)', array('different_name', array('harry', 'jerry'))), + // That static callbacks are parsed into arrays + array('kohana::appify(this)', array(array('kohana', 'appify'), array('this'))), + // Spaces are preserved in parameters + array('deal::make(me, my mate )', array(array('deal', 'make'), array('me', ' my mate '))) + // TODO: add more cases + ); + } + + /** + * Tests Arr::callback() + * + * @test + * @dataProvider provider_callback + * @param string $str String to parse + * @param array $expected Callback and its parameters + */ + public function test_callback($str, $expected) + { + $result = Arr::callback($str); + + $this->assertSame(2, count($result)); + $this->assertSame($expected, $result); + } + + /** + * Provides test data for test_extract + * + * @return array + */ + public function provider_extract() + { + return array( + array( + array('kohana' => 'awesome', 'blueflame' => 'was'), + array('kohana', 'cakephp', 'symfony'), + NULL, + array('kohana' => 'awesome', 'cakephp' => NULL, 'symfony' => NULL) + ), + // I realise noone should EVER code like this in real life, + // but unit testing is very very very very boring + array( + array('chocolate cake' => 'in stock', 'carrot cake' => 'in stock'), + array('carrot cake', 'humble pie'), + 'not in stock', + array('carrot cake' => 'in stock', 'humble pie' => 'not in stock'), + ), + ); + } + + /** + * Tests Arr::extract() + * + * @test + * @dataProvider provider_extract + * @param array $array + * @param array $keys + * @param mixed $default + * @param array $expected + */ + public function test_extract(array $array, array $keys, $default, $expected) + { + $array = Arr::extract($array, $keys, $default); + + $this->assertSame(count($expected), count($array)); + $this->assertSame($expected, $array); + } + + + /** + * Provides test data for test_get() + * + * @return array + */ + public function provider_get() + { + return array( + array(array('uno', 'dos', 'tress'), 1, NULL, 'dos'), + array(array('we' => 'can', 'make' => 'change'), 'we', NULL, 'can'), + + array(array('uno', 'dos', 'tress'), 10, NULL, NULL), + array(array('we' => 'can', 'make' => 'change'), 'he', NULL, NULL), + array(array('we' => 'can', 'make' => 'change'), 'he', 'who', 'who'), + array(array('we' => 'can', 'make' => 'change'), 'he', array('arrays'), array('arrays')), + ); + } + + /** + * Tests Arr::get() + * + * @test + * @dataProvider provider_get() + * @param array $array Array to look in + * @param string|integer $key Key to look for + * @param mixed $default What to return if $key isn't set + * @param mixed $expected The expected value returned + */ + public function test_get(array $array, $key, $default, $expected) + { + $this->assertSame( + $expected, + Arr::get($array, $key, $default) + ); + } + + /** + * Provides test data for test_is_assoc() + * + * @return array + */ + public function provider_is_assoc() + { + return array( + array(array('one', 'two', 'three'), FALSE), + array(array('one' => 'o clock', 'two' => 'o clock', 'three' => 'o clock'), TRUE), + ); + } + + /** + * Tests Arr::is_assoc() + * + * @test + * @dataProvider provider_is_assoc + * @param array $array Array to check + * @param boolean $expected Is $array assoc + */ + public function test_is_assoc(array $array, $expected) + { + $this->assertSame( + $expected, + Arr::is_assoc($array) + ); + } + + public function provider_merge() + { + return array( + // Test how it merges arrays and sub arrays with assoc keys + array( + array('name' => 'mary', 'children' => array('fred', 'paul', 'sally', 'jane')), + array('name' => 'john', 'children' => array('fred', 'paul', 'sally', 'jane')), + array('name' => 'mary', 'children' => array('jane')), + ), + // See how it merges sub-arrays with numerical indexes + array( + array(array('test1','test3'), array('test2','test4')), + array(array('test1'), array('test2')), + array(array('test3'), array('test4')), + ), + array( + array('digits' => array(0, 1, 2, 3)), + array('digits' => array(0, 1)), + array('digits' => array(2, 3)), + ), + // See how it manages merging items with numerical indexes + array( + array(0, 1, 2, 3), + array(0, 1), + array(2, 3), + ), + // Try and get it to merge assoc. arrays recursively + array( + array('foo' => 'bar', array('temp' => 'life')), + array('foo' => 'bin', array('temp' => 'name')), + array('foo' => 'bar', array('temp' => 'life')), + ), + // Bug #3139 + array( + array('foo' => array('bar')), + array('foo' => 'bar'), + array('foo' => array('bar')), + ), + array( + array('foo' => 'bar'), + array('foo' => array('bar')), + array('foo' => 'bar'), + ), + ); + } + + /** + * + * @test + * @dataProvider provider_merge + */ + public function test_merge($expected, $array1, $array2) + { + $this->assertSame( + $expected, + Arr::merge($array1,$array2) + ); + } + + /** + * Provides test data for test_get() + * + * @return array + */ + public function provider_path() + { + $array = array( + 'foobar' => array('definition' => 'lost'), + 'kohana' => 'awesome', + 'users' => array( + 1 => array('name' => 'matt'), + 2 => array('name' => 'john', 'interests' => array('hocky' => array('length' => 2), 'football' => array())), + ), + ); + + return array( + // Tests returns normal values + array($array['foobar'], $array, 'foobar'), + array($array['kohana'], $array, 'kohana'), + array($array['foobar']['definition'], $array, 'foobar.definition'), + // Custom delimiters + array($array['foobar']['definition'], $array, 'foobar/definition', NULL, '/'), + // We should be able to use NULL as a default, returned if the key DNX + array(NULL, $array, 'foobar.alternatives', NULL), + array(NULL, $array, 'kohana.alternatives', NULL), + // Try using a string as a default + array('nothing', $array, 'kohana.alternatives', 'nothing'), + // Make sure you can use arrays as defaults + array(array('far', 'wide'), $array, 'cheese.origins', array('far', 'wide')), + // Ensures path() casts ints to actual integers for keys + array($array['users'][1]['name'], $array, 'users.1.name'), + // Test that a wildcard returns the entire array at that "level" + array($array['users'], $array, 'users.*'), + // Now we check that keys after a wilcard will be processed + array(array(0 => array(0 => 2)), $array, 'users.*.interests.*.length'), + // See what happens when it can't dig any deeper from a wildcard + array(NULL, $array, 'users.*.fans'), + ); + } + + /** + * Tests Arr::get() + * + * @test + * @dataProvider provider_path + * @param string $path The path to follow + * @param mixed $default The value to return if dnx + * @param boolean $expected The expected value + * @param string $delimiter The path delimiter + */ + public function test_path($expected, $array, $path, $default = NULL, $delimiter = NULL) + { + $this->assertSame( + $expected, + Arr::path($array, $path, $default, $delimiter) + ); + } + + /** + * Provides test data for test_range() + * + * @return array + */ + public function provider_range() + { + return array( + array(1, 2), + array(1, 100), + array(25, 10), + ); + } + + /** + * Tests Arr::range() + * + * @dataProvider provider_range + * @param integer $step The step between each value in the array + * @param integer $max The max value of the range (inclusive) + */ + public function test_range($step, $max) + { + $range = Arr::range($step, $max); + + $this->assertSame((int) floor($max / $step), count($range)); + + $current = $step; + + foreach($range as $key => $value) + { + $this->assertSame($key, $value); + $this->assertSame($current, $key); + $this->assertLessThanOrEqual($max, $key); + $current += $step; + } + } + + /** + * Provides test data for test_unshift() + * + * @return array + */ + public function provider_unshift() + { + return array( + array(array('one' => '1', 'two' => '2',), 'zero', '0'), + array(array('step 1', 'step 2', 'step 3'), 'step 0', 'wow') + ); + } + + /** + * Tests Arr::unshift() + * + * @test + * @dataProvider provider_unshift + * @param array $array + * @param string $key + * @param mixed $value + */ + public function test_unshift(array $array, $key, $value) + { + $original = $array; + + Arr::unshift($array, $key, $value); + + $this->assertNotSame($original, $array); + $this->assertSame(count($original) + 1, count($array)); + $this->assertArrayHasKey($key, $array); + + $this->assertSame($value, reset($array)); + $this->assertSame(key($array), $key); + } + + /** + * Provies test data for test_overwrite + * + * @return array Test Data + */ + public function provider_overwrite() + { + return array( + array( + array('name' => 'Henry', 'mood' => 'tired', 'food' => 'waffles', 'sport' => 'checkers'), + array('name' => 'John', 'mood' => 'bored', 'food' => 'bacon', 'sport' => 'checkers'), + array('name' => 'Matt', 'mood' => 'tired', 'food' => 'waffles'), + array('name' => 'Henry', 'age' => 18,), + ), + ); + } + + /** + * + * @test + * @dataProvider provider_overwrite + */ + public function test_overwrite($expected, $arr1, $arr2, $arr3 = array(), $arr4 = array()) + { + $this->assertSame( + $expected, + Arr::overwrite($arr1, $arr2, $arr3, $arr4) + ); + } + + /** + * Provides test data for test_binary_search + * + * @return array Test Data + */ + public function provider_binary_search() + { + return array( + array(2, 'john', array('mary', 'louise', 'john', 'kent')) + ); + } + + /** + * + * @test + * @dataProvider provider_binary_search + */ + public function test_binary_search($expected, $needle, $haystack, $sort = FALSE) + { + $this->assertSame( + $expected, + Arr::binary_search($needle, $haystack, $sort) + ); + } + + /** + * Provides test data for test_map + * + * @return array Test Data + */ + public function provider_map() + { + return array( + array('strip_tags', array('

            foobar

            '), array('foobar')), + array('strip_tags', array(array('

            foobar

            '), array('

            foobar

            ')), array(array('foobar'), array('foobar'))), + ); + } + + /** + * + * @test + * @dataProvider provider_map + */ + public function test_map($method, $source, $expected) + { + $this->assertSame( + $expected, + Arr::map($method, $source) + ); + } + + /** + * Provides test data for test_flatten + * + * @return array Test Data + */ + public function provider_flatten() + { + return array( + array(array('set' => array('one' => 'something'), 'two' => 'other'), array('one' => 'something', 'two' => 'other')), + ); + } + + /** + * + * @test + * @dataProvider provider_flatten + */ + public function test_flatten($source, $expected) + { + $this->assertSame( + $expected, + Arr::flatten($source) + ); + } +} diff --git a/includes/kohana/system/tests/kohana/CLITest.php b/includes/kohana/system/tests/kohana/CLITest.php new file mode 100644 index 00000000..18ef6fa0 --- /dev/null +++ b/includes/kohana/system/tests/kohana/CLITest.php @@ -0,0 +1,167 @@ + + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +Class Kohana_CLITest extends Kohana_Unittest_TestCase +{ + + /** + * Tell PHPUnit to isolate globals during tests + * @var boolean + */ + protected $backupGlobals = TRUE; + + /** + * An array of arguments to put in $_SERVER['argv'] + * @var array + */ + protected $options = array( + '--uri' => 'test/something', + '--we_are_cool', + 'invalid option', + '--version' => '2.23', + '--important' => 'something=true', + '--name' => 'Jeremy Taylor', + ); + + /** + * Setup the enviroment for each test + * + * PHPUnit automatically backups up & restores global variables + */ + public function setUp() + { + parent::setUp(); + + $_SERVER['argv'] = array('index.php'); + + foreach($this->options as $option => $value) + { + if(is_string($option)) + { + $_SERVER['argv'][] = $option.'='.$value; + } + else + { + $_SERVER['argv'][] = $value; + } + } + + $_SERVER['argc'] = count($_SERVER['argv']); + } + + /** + * If for some reason arc != count(argv) then we need + * to fail gracefully. + * + * This test ensures it will + * + * @test + */ + public function test_only_loops_over_available_arguments() + { + ++$_SERVER['argc']; + + $options = CLI::options('uri'); + + $this->assertSame(1, count($options)); + } + + /** + * Options should only parse arguments requested + * + * @test + */ + public function test_only_parses_wanted_arguments() + { + $options = CLI::options('uri'); + + $this->assertSame(1, count($options)); + + $this->assertArrayHasKey('uri', $options); + $this->assertSame($options['uri'], $this->options['--uri']); + } + + /** + * Options should not parse invalid arguments (i.e. not starting with --_ + * + * @test + */ + public function test_does_not_parse_invalid_arguments() + { + $options = CLI::options('uri', 'invalid'); + + $this->assertSame(1, count($options)); + $this->assertArrayHasKey('uri', $options); + $this->assertArrayNotHasKey('invalid', $options); + } + + /** + * Options should parse multiple arguments & values correctly + * + * @test + */ + public function test_parses_multiple_arguments() + { + $options = CLI::options('uri', 'version'); + + $this->assertSame(2, count($options)); + $this->assertArrayHasKey('uri', $options); + $this->assertArrayHasKey('version', $options); + $this->assertSame($this->options['--uri'], $options['uri']); + $this->assertSame($this->options['--version'], $options['version']); + } + + /** + * Options should parse arguments without specified values as NULL + * + * @test + */ + public function test_parses_arguments_without_value_as_null() + { + $options = CLI::options('uri', 'we_are_cool'); + + $this->assertSame(2, count($options)); + $this->assertSame(NULL, $options['we_are_cool']); + } + + /** + * If the argument contains an equals sign then it shouldn't be split + * + * @test + * @ticket 2642 + */ + public function test_cli_only_splits_on_the_first_equals() + { + $options = CLI::options('important'); + + $this->assertSame(1, count($options)); + $this->assertSame('something=true', reset($options)); + } + + /** + * Arguments enclosed with quote marks should be allowed to contain + * spaces + * + * @test + */ + public function test_value_includes_spaces_when_enclosed_with_quotes() + { + $options = CLI::options('name'); + + $this->assertSame(array('name' => 'Jeremy Taylor'), $options); + } +} diff --git a/includes/kohana/system/tests/kohana/ConfigTest.php b/includes/kohana/system/tests/kohana/ConfigTest.php new file mode 100644 index 00000000..3ce129b8 --- /dev/null +++ b/includes/kohana/system/tests/kohana/ConfigTest.php @@ -0,0 +1,243 @@ + + * @author Matt Button + * @copyright (c) 2008-2010 Kohana Team + * @license http://kohanaphp.com/license + */ +Class Kohana_ConfigTest extends Kohana_Unittest_TestCase +{ + + /** + * Calling Kohana_Config::instance() should return the global singleton + * which should persist + * + * @test + * @covers Kohana_Config::instance + */ + public function test_instance_returns_singleton_instance() + { + $this->assertSame(Kohana_Config::instance(), Kohana_Config::instance()); + $this->assertNotSame(new Kohana_Config, Kohana_Config::instance()); + } + + /** + * When a config object is initially created there should be + * no readers attached + * + * @test + * @covers Kohana_Config + */ + public function test_initially_there_are_no_readers() + { + $config = new Kohana_Config; + + $this->assertAttributeSame(array(), '_readers', $config); + } + + /** + * Test that calling attach() on a kohana config object + * adds the specified reader to the config object + * + * @test + * @covers Kohana_Config::attach + */ + public function test_attach_adds_reader_and_returns_this() + { + $config = new Kohana_Config; + $reader = $this->getMock('Kohana_Config_Reader'); + + $this->assertSame($config, $config->attach($reader)); + + $this->assertAttributeContains($reader, '_readers', $config); + } + + /** + * By default (or by passing TRUE as the second parameter) the config object + * should prepend the reader to the front of the readers queue + * + * @test + * @covers Kohana_Config::attach + */ + public function test_attach_adds_reader_to_front_of_queue() + { + $config = new Kohana_Config; + + $reader1 = $this->getMock('Kohana_Config_Reader'); + $reader2 = $this->getMock('Kohana_Config_Reader'); + + $config->attach($reader1); + $config->attach($reader2); + + // Rather than do two assertContains we'll do an assertSame to assert + // the order of the readers + $this->assertAttributeSame(array($reader2, $reader1), '_readers', $config); + + // Now we test using the second parameter + $config = new Kohana_Config; + + $config->attach($reader1); + $config->attach($reader2, TRUE); + + $this->assertAttributeSame(array($reader2, $reader1), '_readers', $config); + } + + /** + * Test that attaching a new reader (and passing FALSE as second param) causes + * phpunit to append the reader rather than prepend + * + * @test + * @covers Kohana_Config::attach + */ + public function test_attach_can_add_reader_to_end_of_queue() + { + $config = new Kohana_Config; + $reader1 = $this->getMock('Kohana_Config_Reader'); + $reader2 = $this->getMock('Kohana_Config_Reader'); + + $config->attach($reader1); + $config->attach($reader2, FALSE); + + $this->assertAttributeSame(array($reader1, $reader2), '_readers', $config); + } + + /** + * Calling detach() on a config object should remove it from the queue of readers + * + * @test + * @covers Kohana_Config::detach + */ + public function test_detach_removes_reader_and_returns_this() + { + $config = new Kohana_Config; + + // Due to the way phpunit mock generator works if you try and mock a class + // that has already been used then it just re-uses the first's name + // + // To get around this we have to specify a totally random name for the second mock object + $reader1 = $this->getMock('Kohana_Config_Reader'); + $reader2 = $this->getMock('Kohana_Config_Reader', array(), array(), 'MY_AWESOME_READER'); + + $config->attach($reader1); + $config->attach($reader2); + + $this->assertSame($config, $config->detach($reader1)); + + $this->assertAttributeNotContains($reader1, '_readers', $config); + $this->assertAttributeContains($reader2, '_readers', $config); + + $this->assertSame($config, $config->detach($reader2)); + + $this->assertAttributeNotContains($reader2, '_readers', $config); + } + + /** + * detach() should return $this even if the specified reader does not exist + * + * @test + * @covers Kohana_Config::detach + */ + public function test_detach_returns_this_even_when_reader_dnx() + { + $config = new Kohana_Config; + $reader = $this->getMock('Kohana_Config_Reader'); + + $this->assertSame($config, $config->detach($reader)); + } + + /** + * If load() is called and there are no readers present then it should throw + * a kohana exception + * + * @test + * @covers Kohana_Config::load + * @expectedException Kohana_Exception + */ + public function test_load_throws_exception_if_there_are_no_readers() + { + // The following code should throw an exception and phpunit will catch / handle it + // (see the @expectedException doccomment) + $config = new Kohana_config; + + $config->load('random'); + } + + /** + * When load() is called it should interrogate each reader in turn until a match + * is found + * + * @test + * @covers Kohana_Config::load + */ + public function test_load_interrogates_each_reader_until_group_found() + { + $config = new Kohana_Config; + $config_group = 'groupy'; + + $reader1 = $this->getMock('Kohana_Config_Reader', array('load')); + $reader1 + ->expects($this->once()) + ->method('load') + ->with($config_group) + ->will($this->returnValue(FALSE)); + + $reader2 = $this->getMock('Kohana_Config_Reader', array('load')); + $reader2 + ->expects($this->once()) + ->method('load') + ->with($config_group) + ->will($this->returnValue($reader2)); + + $reader3 = $this->getMock('Kohana_Config_Reader', array('load')); + $reader3->expects($this->never())->method('load'); + + $config->attach($reader1, FALSE); + $config->attach($reader2, FALSE); + $config->attach($reader3, FALSE); + + // By asserting a return type we're making the test a little less brittle / less likely + // to break due to minor modifications + $this->assertType('Kohana_Config_Reader', $config->load($config_group)); + } + + /** + * Calling load() with a group that doesn't exist, should get it to use the last reader + * to create a new config group + * + * @test + * @covers Kohana_Config::load + */ + public function test_load_returns_new_config_group_if_one_dnx() + { + $config = new Kohana_Config; + $group = 'my_group'; + + $reader1 = $this->getMock('Kohana_Config_Reader'); + $reader2 = $this->getMock('Kohana_Config_Reader', array('load'), array(), 'Kohana_Config_Waffles'); + + // This is a slightly hacky way of doing it, but it works + $reader2 + ->expects($this->exactly(2)) + ->method('load') + ->with($group) + ->will($this->onConsecutiveCalls( + $this->returnValue(FALSE), + $this->returnValue(clone $reader2) + )); + + $config->attach($reader1)->attach($reader2); + + $new_config = $config->load('my_group'); + + $this->assertType('Kohana_Config_Waffles', $new_config); + // Slightly taboo, testing a different api!! + $this->assertSame(array(), $new_config->as_array()); + } +} diff --git a/includes/kohana/system/tests/kohana/CookieTest.php b/includes/kohana/system/tests/kohana/CookieTest.php new file mode 100644 index 00000000..a94ee023 --- /dev/null +++ b/includes/kohana/system/tests/kohana/CookieTest.php @@ -0,0 +1,130 @@ + + * @copyright (c) 2008-2010 Kohana Team + * @license http://kohanaphp.com/license + */ +Class Kohana_CookieTest extends Kohana_Unittest_TestCase +{ + /** + * Provides test data for test_set() + * + * @return array + */ + public function provider_set() + { + return array( + array('foo', 'bar', NULL, TRUE), + array('foo', 'bar', 10, TRUE), + ); + } + + /** + * Tests cookie::set() + * + * @test + * @dataProvider provider_set + * @covers cookie::set + * @param mixed $key key to use + * @param mixed $value value to set + * @param mixed $exp exp to set + * @param boolean $expected Output for cookie::set() + */ + public function test_set($key, $value, $exp, $expected) + { + $this->assertSame($expected, cookie::set($key, $value, $exp)); + } + + /** + * Provides test data for test_get() + * + * @return array + */ + public function provider_get() + { + return array( + array('foo', Cookie::salt('foo', 'bar').'~bar', 'bar'), + array('bar', Cookie::salt('foo', 'bar').'~bar', NULL), + array(NULL, Cookie::salt('foo', 'bar').'~bar', NULL), + ); + } + + /** + * Tests cookie::set() + * + * @test + * @dataProvider provider_get + * @covers cookie::get + * @param mixed $key key to use + * @param mixed $value value to set + * @param boolean $expected Output for cookie::get() + */ + public function test_get($key, $value, $expected) + { + // Force $_COOKIE + if ($key !== NULL) + $_COOKIE[$key] = $value; + + $this->assertSame($expected, cookie::get($key)); + } + + /** + * Provides test data for test_delete() + * + * @return array + */ + public function provider_delete() + { + return array( + array('foo', TRUE), + ); + } + + /** + * Tests cookie::delete() + * + * @test + * @dataProvider provider_delete + * @covers cookie::delete + * @param mixed $key key to use + * @param boolean $expected Output for cookie::delete() + */ + public function test_delete($key, $expected) + { + $this->assertSame($expected, cookie::delete($key)); + } + + /** + * Provides test data for test_salt() + * + * @return array + */ + public function provider_salt() + { + return array( + array('foo', 'bar', '795317c9df04d8061e6e134a9b3487dc9ad69117'), + ); + } + + /** + * Tests cookie::salt() + * + * @test + * @dataProvider provider_salt + * @covers cookie::salt + * @param mixed $key key to use + * @param mixed $value value to salt with + * @param boolean $expected Output for cookie::delete() + */ + public function test_salt($key, $value, $expected) + { + $this->assertSame($expected, cookie::salt($key, $value)); + } +} diff --git a/includes/kohana/system/tests/kohana/CoreTest.php b/includes/kohana/system/tests/kohana/CoreTest.php new file mode 100644 index 00000000..050c3240 --- /dev/null +++ b/includes/kohana/system/tests/kohana/CoreTest.php @@ -0,0 +1,462 @@ + + * @copyright (c) 2008-2010 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_CoreTest extends Kohana_Unittest_TestCase +{ + /** + * Provides test data for test_sanitize() + * + * @return array + */ + public function provider_sanitize() + { + return array( + // $value, $result + array('foo', 'foo'), + array("foo\r\nbar", "foo\nbar"), + array("foo\rbar", "foo\nbar"), + array("Is your name O\'reilly?", "Is your name O'reilly?") + ); + } + + /** + * Tests Kohana::santize() + * + * @test + * @dataProvider provider_sanitize + * @covers Kohana::sanitize + * @param boolean $value Input for Kohana::sanitize + * @param boolean $result Output for Kohana::sanitize + */ + public function test_sanitize($value, $result) + { + $this->setEnvironment(array('Kohana::$magic_quotes' => TRUE)); + + $this->assertSame($result, Kohana::sanitize($value)); + } + + /** + * If a file can't be found then find_file() should return FALSE if + * only a single file was requested, or an empty array if multiple files + * (i.e. configuration files) were requested + * + * @test + * @covers Kohana::find_file + */ + public function test_find_file_returns_false_or_array_on_failure() + { + $this->assertFalse(Kohana::find_file('configy', 'zebra')); + + $this->assertSame(array(), Kohana::find_file('configy', 'zebra', NULL, TRUE)); + } + + /** + * Kohana::list_files() should return an array on success and an empty array on failure + * + * @test + * @covers Kohana::list_files + */ + public function test_list_files_returns_array_on_success_and_failure() + { + $files = Kohana::list_files('config'); + + $this->assertType('array', $files); + $this->assertGreaterThan(3, count($files)); + + $this->assertSame(array(), Kohana::list_files('geshmuck')); + } + + /** + * Tests Kohana::globals() + * + * @test + * @covers Kohana::globals + */ + public function test_globals_removes_user_def_globals() + { + $GLOBALS = array('hackers' => 'foobar','name' => array('','',''), '_POST' => array()); + + Kohana::globals(); + + $this->assertEquals(array('_POST' => array()), $GLOBALS); + } + + /** + * Provides test data for testCache() + * + * @return array + */ + public function provider_cache() + { + return array( + // $value, $result + array('foo', 'hello, world', 10), + array('bar', NULL, 10), + array('bar', NULL, -10), + ); + } + + /** + * Tests Kohana::cache() + * + * @test + * @dataProvider provider_cache + * @covers Kohana::cache + * @param boolean $key Key to cache/get for Kohana::cache + * @param boolean $value Output from Kohana::cache + * @param boolean $lifetime Lifetime for Kohana::cache + */ + public function test_cache($key, $value, $lifetime) + { + Kohana::cache($key, $value, $lifetime); + $this->assertEquals($value, Kohana::cache($key)); + } + + /** + * Provides test data for test_message() + * + * @return array + */ + public function provider_message() + { + return array( + // $value, $result + array(':field must not be empty', 'validate', 'not_empty'), + array( + array( + 'alpha' => ':field must contain only letters', + 'alpha_dash' => ':field must contain only letters and dashes', + 'alpha_numeric' => ':field must contain only letters and numbers', + 'color' => ':field must be a color', + 'credit_card' => ':field must be a credit card number', + 'date' => ':field must be a date', + 'decimal' => ':field must be a decimal with :param1 places', + 'digit' => ':field must be a digit', + 'email' => ':field must be a email address', + 'email_domain' => ':field must contain a valid email domain', + 'exact_length' => ':field must be exactly :param1 characters long', + 'in_array' => ':field must be one of the available options', + 'ip' => ':field must be an ip address', + 'matches' => ':field must be the same as :param1', + 'min_length' => ':field must be at least :param1 characters long', + 'max_length' => ':field must be less than :param1 characters long', + 'phone' => ':field must be a phone number', + 'not_empty' => ':field must not be empty', + 'range' => ':field must be within the range of :param1 to :param2', + 'regex' => ':field does not match the required format', + 'url' => ':field must be a url', + ), + 'validate', NULL, + ), + ); + } + + /** + * Tests Kohana::message() + * + * @test + * @dataProvider provider_message + * @covers Kohana::message + * @param boolean $expected Output for Kohana::message + * @param boolean $file File to look in for Kohana::message + * @param boolean $key Key for Kohana::message + */ + public function test_message($expected, $file, $key) + { + $this->assertEquals($expected, Kohana::message($file, $key)); + } + + /** + * Provides test data for test_error_handler() + * + * @return array + */ + public function provider_error_handler() + { + return array( + array(1, 'Foobar', 'foobar.php', __LINE__), + ); + } + + /** + * Tests Kohana::error_handler() + * + * @test + * @dataProvider provider_error_handler + * @covers Kohana::error_handler + * @param boolean $code Input for Kohana::sanitize + * @param boolean $error Input for Kohana::sanitize + * @param boolean $file Input for Kohana::sanitize + * @param boolean $line Output for Kohana::sanitize + */ + public function test_error_handler($code, $error, $file, $line) + { + $error_level = error_reporting(); + error_reporting(E_ALL); + try + { + Kohana::error_handler($code, $error, $file, $line); + } + catch (Exception $e) + { + $this->assertEquals($code, $e->getCode()); + $this->assertEquals($error, $e->getMessage()); + } + error_reporting($error_level); + } + + /** + * Provides test data for testExceptionHandler() + * + * @return array + */ + public function provider_exception_handler() + { + return array( + // $exception_type, $message, $is_cli, $expected + array('Kohana_Exception', 'hello, world!', TRUE, TRUE, 'hello, world!'), + array('ErrorException', 'hello, world!', TRUE, TRUE, 'hello, world!'), + // #3016 + array('Kohana_Exception', '', FALSE, TRUE, '<hello, world!>'), + ); + } + + /** + * Tests Kohana::exception_handler() + * + * @test + * @dataProvider provider_exception_handler + * @covers Kohana::exception_handler + * @param boolean $exception_type Exception type to throw + * @param boolean $message Message to pass to exception + * @param boolean $is_cli Use cli mode? + * @param boolean $expected Output for Kohana::exception_handler + * @param string $expexcted_message What to look for in the output string + */ + public function teste_exception_handler($exception_type, $message, $is_cli, $expected, $expected_message) + { + try + { + Kohana::$is_cli = $is_cli; + throw new $exception_type($message); + } + catch (Exception $e) + { + ob_start(); + $this->assertEquals($expected, Kohana::exception_handler($e)); + $view = ob_get_contents(); + ob_clean(); + $this->assertContains($expected_message, $view); + } + + Kohana::$is_cli = TRUE; + } + + /** + * Provides test data for test_debug() + * + * @return array + */ + public function provider_debug() + { + return array( + // $exception_type, $message, $is_cli, $expected + array(array('foobar'), "
            array(1) (\n    0 => string(6) \"foobar\"\n)
            "), + ); + } + + /** + * Tests Kohana::debug() + * + * @test + * @dataProvider provider_debug + * @covers Kohana::debug + * @param boolean $thing The thing to debug + * @param boolean $expected Output for Kohana::debug + */ + public function testdebug($thing, $expected) + { + $this->assertEquals($expected, Kohana::debug($thing)); + } + + /** + * Provides test data for testDebugPath() + * + * @return array + */ + public function provider_debug_path() + { + return array( + array( + Kohana::find_file('classes', 'kohana'), + 'SYSPATH'.DIRECTORY_SEPARATOR.'classes'.DIRECTORY_SEPARATOR.'kohana.php' + ), + array( + Kohana::find_file('classes', $this->dirSeparator('kohana/unittest/runner')), + $this->dirSeparator('MODPATH/unittest/classes/kohana/unittest/runner.php') + ), + ); + } + + /** + * Tests Kohana::debug_path() + * + * @test + * @dataProvider provider_debug_path + * @covers Kohana::debug_path + * @param boolean $path Input for Kohana::debug_path + * @param boolean $expected Output for Kohana::debug_path + */ + public function testDebugPath($path, $expected) + { + $this->assertEquals($expected, Kohana::debug_path($path)); + } + + /** + * Provides test data for test_modules_sets_and_returns_valid_modules() + * + * @return array + */ + public function provider_modules_sets_and_returns_valid_modules() + { + return array( + array(array(), array()), + array(array('unittest' => MODPATH.'fo0bar'), array()), + array(array('unittest' => MODPATH.'unittest'), array('unittest' => $this->dirSeparator(MODPATH.'unittest/'))), + ); + } + + /** + * Tests Kohana::modules() + * + * @test + * @dataProvider provider_modules_sets_and_returns_valid_modules + * @covers Kohana::modules + * @param boolean $source Input for Kohana::modules + * @param boolean $expected Output for Kohana::modules + */ + public function test_modules_sets_and_returns_valid_modules($source, $expected) + { + $modules = Kohana::modules(); + + $this->assertEquals($expected, Kohana::modules($source)); + + Kohana::modules($modules); + } + + /** + * To make the tests as portable as possible this just tests that + * you get an array of modules when you can Kohana::modules() and that + * said array contains unittest + * + * @test + * @covers Kohana::modules + */ + public function test_modules_returns_array_of_modules() + { + $modules = Kohana::modules(); + + $this->assertType('array', $modules); + + $this->assertArrayHasKey('unittest', $modules); + } + + /** + * Tests Kohana::include_paths() + * + * The include paths must contain the apppath and syspath + * @test + * @covers Kohana::include_paths + */ + public function test_include_paths() + { + $include_paths = Kohana::include_paths(); + $modules = Kohana::modules(); + + $this->assertType('array', $include_paths); + + // We must have at least 2 items in include paths (APP / SYS) + $this->assertGreaterThan(2, count($include_paths)); + // Make sure said paths are in the include paths + // And make sure they're in the correct positions + $this->assertSame(APPPATH, reset($include_paths)); + $this->assertSame(SYSPATH, end($include_paths)); + + foreach($modules as $module) + { + $this->assertContains($module, $include_paths); + } + } + + /** + * Provides test data for test_exception_text() + * + * @return array + */ + public function provider_exception_text() + { + return array( + array(new Kohana_Exception('foobar'), $this->dirSeparator('Kohana_Exception [ 0 ]: foobar ~ SYSPATH/tests/kohana/CoreTest.php [ '.__LINE__.' ]')), + ); + } + + /** + * Tests Kohana::exception_text() + * + * @test + * @dataProvider provider_exception_text + * @covers Kohana::exception_text + * @param object $exception exception to test + * @param string $expected expected output + */ + public function test_exception_text($exception, $expected) + { + $this->assertEquals($expected, Kohana::exception_text($exception)); + } + + /** + * Provides test data for test_dump() + * + * @return array + */ + public function provider_dump() + { + return array( + array('foobar', 128, 'string(6) "foobar"'), + array('foobar', 2, 'string(6) "fo …"'), + array(NULL, 128, 'NULL'), + array(TRUE, 128, 'bool TRUE'), + array(array('foobar'), 128, "array(1) (\n 0 => string(6) \"foobar\"\n)"), + array(new StdClass, 128, "object stdClass(0) {\n}"), + array("fo\x6F\xFF\x00bar\x8F\xC2\xB110", 128, 'string(10) "foobar±10"'), + ); + } + + /** + * Tests Kohana::dump() + * + * @test + * @dataProvider provider_dump + * @covers Kohana::dump + * @covers Kohana::_dump + * @param object $exception exception to test + * @param string $expected expected output + */ + public function test_dump($input, $length, $expected) + { + $this->assertEquals($expected, Kohana::dump($input, $length)); + } +} diff --git a/includes/kohana/system/tests/kohana/DateTest.php b/includes/kohana/system/tests/kohana/DateTest.php new file mode 100644 index 00000000..6e527bec --- /dev/null +++ b/includes/kohana/system/tests/kohana/DateTest.php @@ -0,0 +1,679 @@ + + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +Class Kohana_DateTest extends Kohana_Unittest_TestCase +{ + /** + * Provides test data for test_offset() + * + * @return array + */ + public function provider_offset() + { + return array( + array( + -18000, + 'America/Chicago', + 'GMT', + ), + ); + } + + /** + * Tests Date::offset() + * + * @test + * @dataProvider provider_offset + * @covers Date::offset + * @param integer $expected Expected offset + * @param string $remote Remote TZ + * @param string $local Local TZ + * @param integer $now Current timestamp + */ + public function test_offset($expected, $remote, $local, $now = NULL) + { + $this->assertSame($expected, Date::offset($remote, $local, $now)); + } + + /** + * Provides test data for test_date() + * + * @return array + */ + public function provider_am_pm() + { + return array( + // All possible values + array(0, 'AM'), + array(1, 'AM'), + array(2, 'AM'), + array(3, 'AM'), + array(4, 'AM'), + array(5, 'AM'), + array(6, 'AM'), + array(7, 'AM'), + array(8, 'AM'), + array(9, 'AM'), + array(10, 'AM'), + array(11, 'AM'), + array(12, 'PM'), + array(13, 'PM'), + array(14, 'PM'), + array(15, 'PM'), + array(16, 'PM'), + array(17, 'PM'), + array(18, 'PM'), + array(19, 'PM'), + array(20, 'PM'), + array(21, 'PM'), + array(22, 'PM'), + array(23, 'PM'), + array(24, 'PM'), + // ampm doesn't validate the hour, so I don't think we should test it.. + // test strings are converted + array('0', 'AM'), + array('12', 'PM'), + ); + } + + /** + * Tests Date::ampm() + * + * @test + * @covers Date::ampm + * @dataProvider provider_am_pm + * @param $hour + * @param $expected + */ + public function test_am_pm($hour, $expected) + { + $this->assertSame( + $expected, + Date::ampm($hour) + ); + } + + /** + * Provides test data for test_adjust() + * + * @return array + */ + public function provider_adjust() + { + return array( + // Might as well test all possibilities + array(1, 'am', '01'), + array(2, 'am', '02'), + array(3, 'am', '03'), + array(4, 'am', '04'), + array(5, 'am', '05'), + array(6, 'am', '06'), + array(7, 'am', '07'), + array(8, 'am', '08'), + array(9, 'am', '09'), + array(10, 'am', '10'), + array(11, 'am', '11'), + array(12, 'am', '00'), + array(1, 'pm', '13'), + array(2, 'pm', '14'), + array(3, 'pm', '15'), + array(4, 'pm', '16'), + array(5, 'pm', '17'), + array(6, 'pm', '18'), + array(7, 'pm', '19'), + array(8, 'pm', '20'), + array(9, 'pm', '21'), + array(10, 'pm', '22'), + array(11, 'pm', '23'), + array(12, 'pm', '12'), + // It should also work with strings instead of ints + array('10', 'pm', '22'), + array('10', 'am', '10'), + ); + } + + /** + * Tests Date::ampm() + * + * @test + * @dataProvider provider_adjust + * @param integer $hour Hour in 12 hour format + * @param string $ampm Either am or pm + * @param string $expected Expected result + */ + public function test_adjust($hour, $ampm, $expected) + { + $this->assertSame( + $expected, + Date::adjust($hour, $ampm) + ); + } + + /** + * Provides test data for test_days() + * + * @return array + */ + public function provider_days() + { + return array( + // According to "the rhyme" these should be the same every year + array(9, FALSE, 30), + array(4, FALSE, 30), + array(6, FALSE, 30), + array(11, FALSE, 30), + array(1, FALSE, 31), + array(3, FALSE, 31), + array(5, FALSE, 31), + array(7, FALSE, 31), + array(8, FALSE, 31), + array(10, FALSE, 31), + // February is such a pain + array(2, 2001, 28), + array(2, 2000, 29), + array(2, 2012, 29), + ); + } + + /** + * Tests Date::days() + * + * @test + * @covers Date::days + * @dataProvider provider_days + * @param integer $month + * @param integer $year + * @param integer $expected + */ + public function test_days($month, $year, $expected) + { + $days = Date::days($month, $year); + + $this->assertSame( + $expected, + count($days) + ); + + // This should be a mirrored array, days => days + for($i = 1; $i <= $expected; ++$i) + { + $this->assertArrayHasKey($i, $days); + // Combining the type check into this saves about 400-500 assertions! + $this->assertSame((string) $i, $days[$i]); + } + } + + /** + * Provides test data for test_formatted_time() + * + * @return array + */ + public function provider_formatted_time() + { + return array( + // Test the default format + array('2010-04-16 17:00:00', '5:00PM 16th April 2010'), + // Now we use our own format + // Binary date! + array('01/01/2010 01:00', '1AM 1st January 2010', 'd/m/Y H:i'), + ); + } + + /** + * Tests Date::formatted_time() + * + * @test + * @dataProvider provider_formatted_time + * @covers Date::formatted_time + * @ticket 3035 + * @param string $expected Expected output + * @param string|integer $datetime_str The datetime timestamp / string + * @param string|null $timestamp_format The output format + */ + public function test_formatted_time($expected, $datetime_str, $timestamp_format = NULL) + { + $timestamp = Date::formatted_time($datetime_str, $timestamp_format); + + $this->assertSame($expected, $timestamp); + } + + /** + * Tests Date::months() + * + * @test + * @covers Date::months + */ + public function test_months() + { + $months = Date::months(); + + $this->assertSame(12, count($months)); + + for($i = 1; $i <= 12; ++$i) + { + $this->assertArrayHasKey($i, $months); + $this->assertSame((string) $i, $months[$i]); + } + } + + /** + * Provides test data for test_span() + * + * @return array + */ + public function provider_span() + { + $time = time(); + return array( + // Test that it must specify an output format + array( + $time, + $time, + '', + FALSE + ), + // Test that providing only one output just returns that output + array( + $time - 30, + $time, + 'seconds', + 30 + ), + // Random tests + array( + $time - 30, + $time, + 'years,months,weeks,days,hours,minutes,seconds', + array('years' => 0, 'months' => 0, 'weeks' => 0, 'days' => 0, 'hours' => 0, 'minutes' => 0, 'seconds' => 30), + ), + array( + $time - (60 * 60 * 24 * 782) + (60 * 25), + $time, + 'years,months,weeks,days,hours,minutes,seconds', + array('years' => 2, 'months' => 1, 'weeks' => 3, 'days' => 0, 'hours' => 1, 'minutes' => 28, 'seconds' => 24), + ), + // Should be able to compare with the future & that it only uses formats specified + array( + $time + (60 * 60 * 24 * 15) + (60 * 5), + $time, + 'weeks,days,hours,minutes,seconds', + array('weeks' => 2, 'days' => 1, 'hours' => 0, 'minutes' => 5, 'seconds' => 0), + ), + array( + // Add a bit of extra time to account for phpunit processing + $time + (14 * 31 * 24* 60 * 60) + (79 * 80), + NULL, + 'months,years', + array('months' => 2, 'years' => 1), + ), + ); + } + + /** + * Tests Date::span() + * + * @test + * @covers Date::span + * @dataProvider provider_span + * @param integer $time1 Time in the past + * @param integer $time2 Time to compare against + * @param string $output Units to output + * @param array $expected Array of $outputs => values + */ + public function test_span($time1, $time2, $output, $expected) + { + $this->assertSame( + $expected, + Date::span($time1, $time2, $output) + ); + } + + /** + * Provides test data to test_fuzzy_span + * + * This test data is provided on the assumption that it + * won't take phpunit more than 30 seconds to get the + * data from this provider to the test... ;) + * + * @return array Test Data + */ + public function provider_fuzzy_span() + { + return array( + array('moments ago', time() - 30), + array('in moments', time() + 30), + + array('a few minutes ago', time() - 10*60), + array('in a few minutes', time() + 10*60), + + array('less than an hour ago', time() - 45*60), + array('in less than an hour', time() + 45*60), + + array('a couple of hours ago', time() - 2*60*60), + array('in a couple of hours', time() + 2*60*60), + + array('less than a day ago', time() - 12*60*60), + array('in less than a day', time() + 12*60*60), + + array('about a day ago', time() - 30*60*60), + array('in about a day', time() + 30*60*60), + + array('a couple of days ago', time() - 3*24*60*60), + array('in a couple of days', time() + 3*24*60*60), + + array('less than a week ago', time() - 5*24*60*60), + array('in less than a week', time() + 5*24*60*60), + + array('about a week ago', time() - 9*24*60*60), + array('in about a week', time() + 9*24*60*60), + + array('less than a month ago', time() - 20*24*60*60), + array('in less than a month', time() + 20*24*60*60), + + array('about a month ago', time() - 40*24*60*60), + array('in about a month', time() + 40*24*60*60), + + array('a couple of months ago', time() - 3*30*24*60*60), + array('in a couple of months', time() + 3*30*24*60*60), + + array('less than a year ago', time() - 7*31*24*60*60), + array('in less than a year', time() + 7*31*24*60*60), + + array('about a year ago', time() - 18*31*24*60*60), + array('in about a year', time() + 18*31*24*60*60), + + array('a couple of years ago', time() - 3*12*31*24*60*60), + array('in a couple of years', time() + 3*12*31*24*60*60), + + array('a few years ago', time() - 5*12*31*24*60*60), + array('in a few years', time() + 5*12*31*24*60*60), + + array('about a decade ago', time() - 11*12*31*24*60*60), + array('in about a decade', time() + 11*12*31*24*60*60), + + array('a couple of decades ago', time() - 20*12*31*24*60*60), + array('in a couple of decades', time() + 20*12*31*24*60*60), + + array('several decades ago', time() - 50*12*31*24*60*60), + array('in several decades', time() + 50*12*31*24*60*60), + + array('a long time ago', time() - pow(10,10)), + array('in a long time', time() + pow(10,10)), + ); + } + + /** + * Test of Date::fuzy_span() + * + * @test + * @dataProvider provider_fuzzy_span + * @param string $expected Expected output + * @param integer $timestamp Timestamp to use + */ + public function test_fuzzy_span($expected, $timestamp) + { + $this->assertSame( + $expected, + Date::fuzzy_span($timestamp) + ); + } + + /** + * Provides test data for test_years() + * + * @return array Test Data + */ + public function provider_years() + { + return array( + array( + array ( + 2005 => '2005', + 2006 => '2006', + 2007 => '2007', + 2008 => '2008', + 2009 => '2009', + 2010 => '2010', + 2011 => '2011', + 2012 => '2012', + 2013 => '2013', + 2014 => '2014', + 2015 => '2015', + ), + 2005, + 2015 + ), + ); + } + + /** + * Tests Data::years() + * + * @test + * @dataProvider provider_years + */ + public function test_years($expected, $start = FALSE, $end = FALSE) + { + $this->assertSame( + $expected, + Date::years($start, $end) + ); + } + + public function provider_hours() + { + return array( + array( + array( + 1 => '1', + 2 => '2', + 3 => '3', + 4 => '4', + 5 => '5', + 6 => '6', + 7 => '7', + 8 => '8', + 9 => '9', + 10 => '10', + 11 => '11', + 12 => '12', + ), + ), + ); + } + + /** + * Test for Date::hours + * + * @test + * @dataProvider provider_hours + */ + public function test_hours($expected, $step = 1, $long = FALSE, $start = NULL) + { + $this->assertSame( + $expected, + Date::hours($step, $long, $start) + ); + } + + /** + * Provides test data for test_seconds + * + * @return array Test data + */ + public function provider_seconds() + { + return array( + array( + // Thank god for var_export() + array ( + 0 => '00', 1 => '01', 2 => '02', 3 => '03', 4 => '04', + 5 => '05', 6 => '06', 7 => '07', 8 => '08', 9 => '09', + 10 => '10', 11 => '11', 12 => '12', 13 => '13', 14 => '14', + 15 => '15', 16 => '16', 17 => '17', 18 => '18', 19 => '19', + 20 => '20', 21 => '21', 22 => '22', 23 => '23', 24 => '24', + 25 => '25', 26 => '26', 27 => '27', 28 => '28', 29 => '29', + 30 => '30', 31 => '31', 32 => '32', 33 => '33', 34 => '34', + 35 => '35', 36 => '36', 37 => '37', 38 => '38', 39 => '39', + 40 => '40', 41 => '41', 42 => '42', 43 => '43', 44 => '44', + 45 => '45', 46 => '46', 47 => '47', 48 => '48', 49 => '49', + 50 => '50', 51 => '51', 52 => '52', 53 => '53', 54 => '54', + 55 => '55', 56 => '56', 57 => '57', 58 => '58', 59 => '59', + ), + 1, + 0, + 60 + ), + ); + } + + /** + * + * @test + * @dataProvider provider_seconds + * @covers Date::seconds + */ + public function test_seconds($expected, $step = 1, $start = 0, $end = 60) + { + $this->assertSame( + $expected, + Date::seconds($step, $start, $end) + ); + } + + /** + * Provides test data for test_minutes + * + * @return array Test data + */ + public function provider_minutes() + { + return array( + array( + array( + 0 => '00', 5 => '05', 10 => '10', + 15 => '15', 20 => '20', 25 => '25', + 30 => '30', 35 => '35', 40 => '40', + 45 => '45', 50 => '50', 55 => '55', + ), + 5, + ), + ); + } + + /** + * + * @test + * @dataProvider provider_minutes + */ + public function test_minutes($expected, $step) + { + $this->assertSame( + $expected, + Date::minutes($step) + ); + } + + /** + * This tests that the minutes helper defaults to using a $step of 5 + * and thus returns an array of 5 minute itervals + * + * @test + * @covers Date::minutes + */ + public function test_minutes_defaults_to_using_step_of5() + { + $minutes = array( + 0 => '00', 5 => '05', 10 => '10', + 15 => '15', 20 => '20', 25 => '25', + 30 => '30', 35 => '35', 40 => '40', + 45 => '45', 50 => '50', 55 => '55', + ); + + $this->assertSame( + $minutes, + Date::minutes() + ); + } + + /** + * Provids for test_unix2dos + * + * @return array Test Data + */ + public function provider_unix2dos() + { + return array( + array( + 1024341746, + 1281786936 + ), + array( + 2162688, + 315554400 + ) + ); + } + + /** + * Test Date::unix2dos() + * + * You should always pass a timestamp as otherwise the current + * date/time would be used and that's oviously variable + * + * Geert seems to be the only person who knows how unix2dos() works + * so we just throw in some random values and see what happens + * + * @test + * @dataProvider provider_unix2dos + * @covers Date::unix2dos + * @param integer $expected Expected output + * @param integer $timestamp Input timestamp + */ + public function test_unix2dos($expected, $timestamp) + { + $this->assertSame($expected, Date::unix2dos($timestamp)); + } + + /** + * Provides test data for test_dos2unix + * + * @return array Test data + */ + public function provider_dos2unix() + { + return array( + array( + 1281786936, + 1024341746, + ), + array( + 315554400, + 2162688, + ), + ); + } + + /** + * Tests Date::dos2unix + * + * @test + * @dataProvider provider_dos2unix + * @param integer $expected Expected output + * @param integer $timestamp Input timestamp + */ + public function test_dos2unix($expected, $timestamp) + { + $this->assertEquals($expected, Date::dos2unix($timestamp)); + } +} diff --git a/includes/kohana/system/tests/kohana/FeedTest.php b/includes/kohana/system/tests/kohana/FeedTest.php new file mode 100644 index 00000000..75f1ccdb --- /dev/null +++ b/includes/kohana/system/tests/kohana/FeedTest.php @@ -0,0 +1,121 @@ + + * @copyright (c) 2008-2010 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_FeedTest extends Kohana_Unittest_TestCase +{ + /** + * Provides test data for test_parse() + * + * @return array + */ + public function provider_parse() + { + return array( + // $source, $expected + array('http://dev.kohanaframework.org/projects/kohana3/activity.atom', 15), + ); + } + + /** + * Tests that Feed::parse gets the correct number of elements + * + * @test + * @dataProvider provider_parse + * @covers feed::parse + * @param string $source URL to test + * @param integer $expected Count of items + */ + public function test_parse($source, $expected) + { + if ( ! $this->hasInternet()) + $this->markTestSkipped('An internet connection is required for this test'); + + $this->assertEquals($expected, count(feed::parse($source))); + } + + /** + * Provides test data for test_create() + * + * @return array + */ + public function provider_create() + { + $info = array('pubDate' => 123, 'image' => array('link' => 'http://kohanaframework.org/image.png', 'url' => 'http://kohanaframework.org/', 'title' => 'title')); + + return array( + // $source, $expected + array($info, array('foo' => array('foo' => 'bar', 'pubDate' => 123, 'link' => 'foo')), array('_SERVER' => array('HTTP_HOST' => 'localhost')+$_SERVER), + array( + 'tag' => 'channel', + 'descendant' => array( + 'tag' => 'item', + 'child' => array( + 'tag' => 'foo', + 'content' => 'bar' + ) + ) + ), + array( + $this->matcher_composer($info, 'image', 'link'), + $this->matcher_composer($info, 'image', 'url'), + $this->matcher_composer($info, 'image', 'title') + ) + ), + ); + } + + /** + * Helper for handy matcher composing + * + * @param array $data + * @param string $tag + * @param string $child + * @return array + */ + private function matcher_composer($data, $tag, $child) + { + return array( + 'tag' => 'channel', + 'descendant' => array( + 'tag' => $tag, + 'child' => array( + 'tag' => $child, + 'content' => $data[$tag][$child] + ) + ) + ); + } + + /** + * @test + * + * @dataProvider provider_create + * + * @covers feed::create + * + * @param string $info info to pass + * @param integer $items items to add + * @param integer $matcher output + */ + public function test_create($info, $items, $enviroment, $matcher_item, $matchers_image) + { + $this->setEnvironment($enviroment); + + $this->assertTag($matcher_item, feed::create($info, $items), '', FALSE); + + foreach ($matchers_image as $matcher_image) + { + $this->assertTag($matcher_image, feed::create($info, $items), '', FALSE); + } + } +} diff --git a/includes/kohana/system/tests/kohana/FileTest.php b/includes/kohana/system/tests/kohana/FileTest.php new file mode 100644 index 00000000..ebe9b695 --- /dev/null +++ b/includes/kohana/system/tests/kohana/FileTest.php @@ -0,0 +1,73 @@ + + * @copyright (c) 2008-2010 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_FileTest extends Kohana_Unittest_Testcase +{ + /** + * Provides test data for test_sanitize() + * + * @return array + */ + public function provider_mime() + { + return array( + // $value, $result + array(Kohana::find_file('classes', 'file')), + array(Kohana::find_file('tests', 'test_data/github', 'png')), + ); + } + + /** + * Tests File::mime() + * + * @test + * @dataProvider provider_mime + * @param boolean $input Input for File::mime + * @param boolean $expected Output for File::mime + */ + public function test_mime($input) + { + $this->assertSame(1, preg_match('/^(?:application|audio|image|message|multipart|text|video)\/[a-z.+0-9-]+$/i', File::mime($input))); + } + + /** + * Provides test data for test_split_join() + * + * @return array + */ + public function provider_split_join() + { + return array( + // $value, $result + array(Kohana::find_file('tests', 'test_data/github', 'png'), .01, 1), + ); + } + + /** + * Tests File::mime() + * + * @test + * @dataProvider provider_split_join + * @param boolean $input Input for File::split + * @param boolean $peices Input for File::split + * @param boolean $expected Output for File::splut + */ + public function test_split_join($input, $peices, $expected) + { + $this->assertSame($expected, File::split($input, $peices)); + $this->assertSame($expected, File::join($input)); + + foreach (glob(Kohana::find_file('tests', 'test_data/github', 'png') . '.*') as $file) unlink($file); + } +} diff --git a/includes/kohana/system/tests/kohana/FormTest.php b/includes/kohana/system/tests/kohana/FormTest.php new file mode 100644 index 00000000..5967546c --- /dev/null +++ b/includes/kohana/system/tests/kohana/FormTest.php @@ -0,0 +1,372 @@ + + * @copyright (c) 2008-2010 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_FormTest extends Kohana_Unittest_Testcase +{ + /** + * Defaults for this test + * @var array + */ + protected $environmentDefault = array( + 'Kohana::$base_url' => '/', + 'HTTP_HOST' => 'kohanaframework.org', + ); + + /** + * Provides test data for test_open() + * + * @return array + */ + public function provider_open() + { + return array( + // $value, $result + #array(NULL, NULL, '
            '), // Fails because of Request::$current + array('foo', NULL), + array('', NULL), + array('foo', array('method' => 'get')), + ); + } + + /** + * Tests Form::open() + * + * @test + * @dataProvider provider_open + * @param boolean $input Input for Form::open + * @param boolean $expected Output for Form::open + */ + public function test_open($action, $attributes) + { + $tag = Form::open($action, $attributes); + + $matcher = array( + 'tag' => 'form', + 'attributes' => array( + 'method' => 'post', + 'accept-charset' => 'utf-8', + ), + ); + + if($attributes !== NULL) + $matcher['attributes'] = $attributes + $matcher['attributes']; + + $this->assertTag($matcher, $tag); + } + + /** + * Tests Form::close() + * + * @test + */ + public function test_close() + { + $this->assertSame('
            ', Form::close()); + } + + /** + * Provides test data for test_input() + * + * @return array + */ + public function provider_input() + { + return array( + // $value, $result + array('input', 'foo', 'bar', NULL, 'input'), + array('input', 'foo', NULL, NULL, 'input'), + array('hidden', 'foo', 'bar', NULL, 'hidden'), + array('password', 'foo', 'bar', NULL, 'password'), + ); + } + + /** + * Tests Form::input() + * + * @test + * @dataProvider provider_input + * @param boolean $input Input for Form::input + * @param boolean $expected Output for Form::input + */ + public function test_input($type, $name, $value, $attributes) + { + $matcher = array( + 'tag' => 'input', + 'attributes' => array('name' => $name, 'type' => $type) + ); + + // Form::input creates a text input + if($type === 'input') + $matcher['attributes']['type'] = 'text'; + + // NULL just means no value + if($value !== NULL) + $matcher['attributes']['value'] = $value; + + // Add on any attributes + if(is_array($attributes)) + $matcher['attributes'] = $attributes + $matcher['attributes']; + + $tag = Form::$type($name, $value, $attributes); + + $this->assertTag($matcher, $tag, $tag); + } + + /** + * Provides test data for test_file() + * + * @return array + */ + public function provider_file() + { + return array( + // $value, $result + array('foo', NULL, ''), + ); + } + + /** + * Tests Form::file() + * + * @test + * @dataProvider provider_file + * @param boolean $input Input for Form::file + * @param boolean $expected Output for Form::file + */ + public function test_file($name, $attributes, $expected) + { + $this->assertSame($expected, Form::file($name, $attributes)); + } + + /** + * Provides test data for test_check() + * + * @return array + */ + public function provider_check() + { + return array( + // $value, $result + array('checkbox', 'foo', NULL, FALSE, NULL), + array('checkbox', 'foo', NULL, TRUE, NULL), + array('checkbox', 'foo', 'bar', TRUE, NULL), + + array('radio', 'foo', NULL, FALSE, NULL), + array('radio', 'foo', NULL, TRUE, NULL), + array('radio', 'foo', 'bar', TRUE, NULL), + ); + } + + /** + * Tests Form::check() + * + * @test + * @dataProvider provider_check + * @param boolean $input Input for Form::check + * @param boolean $expected Output for Form::check + */ + public function test_check($type, $name, $value, $checked, $attributes) + { + $matcher = array('tag' => 'input', 'attributes' => array('name' => $name, 'type' => $type)); + + if($value !== NULL) + $matcher['attributes']['value'] = $value; + + if(is_array($attributes)) + $matcher['attributes'] = $attributes + $matcher['attributes']; + + if($checked === TRUE) + $matcher['attributes']['checked'] = 'checked'; + + $tag = Form::$type($name, $value, $checked, $attributes); + $this->assertTag($matcher, $tag, $tag); + } + + /** + * Provides test data for test_text() + * + * @return array + */ + public function provider_text() + { + return array( + // $value, $result + array('textarea', 'foo', 'bar', NULL), + array('textarea', 'foo', 'bar', array('rows' => 20, 'cols' => 20)), + array('button', 'foo', 'bar', NULL), + array('label', 'foo', 'bar', NULL), + array('label', 'foo', NULL, NULL), + ); + } + + /** + * Tests Form::textarea() + * + * @test + * @dataProvider provider_text + * @param boolean $input Input for Form::textarea + * @param boolean $expected Output for Form::textarea + */ + public function test_text($type, $name, $body, $attributes) + { + $matcher = array( + 'tag' => $type, + 'attributes' => array(), + 'content' => $body, + ); + + if($type !== 'label') + $matcher['attributes'] = array('name' => $name); + else + $matcher['attributes'] = array('for' => $name); + + + if(is_array($attributes)) + $matcher['attributes'] = $attributes + $matcher['attributes']; + + $tag = Form::$type($name, $body, $attributes); + + $this->assertTag($matcher, $tag, $tag); + } + + + /** + * Provides test data for test_select() + * + * @return array + */ + public function provider_select() + { + return array( + // $value, $result + array('foo', NULL, NULL, ""), + array('foo', array('bar' => 'bar'), NULL, ""), + array('foo', array('bar' => 'bar'), 'bar', ""), + array('foo', array('bar' => array('foo' => 'bar')), NULL, ""), + array('foo', array('bar' => array('foo' => 'bar')), 'foo', ""), + // #2286 + array('foo', array('bar' => 'bar', 'unit' => 'test', 'foo' => 'foo'), array('bar', 'foo'), ""), + ); + } + + /** + * Tests Form::select() + * + * @test + * @dataProvider provider_select + * @param boolean $input Input for Form::select + * @param boolean $expected Output for Form::select + */ + public function test_select($name, $options, $selected, $expected) + { + // Much more efficient just to assertSame() rather than assertTag() on each element + $this->assertSame($expected, Form::select($name, $options, $selected)); + } + + /** + * Provides test data for test_submit() + * + * @return array + */ + public function provider_submit() + { + return array( + // $value, $result + array('foo', 'Foobar!', ''), + ); + } + + /** + * Tests Form::submit() + * + * @test + * @dataProvider provider_submit + * @param boolean $input Input for Form::submit + * @param boolean $expected Output for Form::submit + */ + public function test_submit($name, $value, $expected) + { + $matcher = array( + 'tag' => 'input', + 'attributes' => array('name' => $name, 'type' => 'submit', 'value' => $value) + ); + + $this->assertTag($matcher, Form::submit($name, $value)); + } + + + /** + * Provides test data for test_image() + * + * @return array + */ + public function provider_image() + { + return array( + // $value, $result + array('foo', 'bar', array('src' => 'media/img/login.png'), ''), + ); + } + + /** + * Tests Form::image() + * + * @test + * @dataProvider provider_image + * @param boolean $name Input for Form::image + * @param boolean $value Input for Form::image + * @param boolean $attributes Input for Form::image + * @param boolean $expected Output for Form::image + */ + public function test_image($name, $value, $attributes, $expected) + { + $this->assertSame($expected, Form::image($name, $value, $attributes)); + } + + /** + * Provides test data for testLabel() + * + * @return array + */ + function providerLabel() + { + return array( + // $value, $result + // Single for provided + array('email', NULL, NULL, ''), + array('email_address', NULL, NULL, ''), + array('email-address', NULL, NULL, ''), + // For and text values provided + array('name', 'First name', NULL, ''), + // with attributes + array('lastname', 'Last name', array('class' => 'text'), ''), + array('lastname', 'Last name', array('class' => 'text', 'id'=>'txt_lastname'), ''), + ); + } + + /** + * Tests Form::label() + * + * @test + * @dataProvider providerLabel + * @param boolean $for Input for Form::label + * @param boolean $text Input for Form::label + * @param boolean $attributes Input for Form::label + * @param boolean $expected Output for Form::label + */ + function testLabel($for, $text, $attributes, $expected) + { + $this->assertSame($expected, Form::label($for, $text, $attributes)); + } +} diff --git a/includes/kohana/system/tests/kohana/HTMLTest.php b/includes/kohana/system/tests/kohana/HTMLTest.php new file mode 100644 index 00000000..7fbb6af0 --- /dev/null +++ b/includes/kohana/system/tests/kohana/HTMLTest.php @@ -0,0 +1,227 @@ + + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +Class Kohana_HTMLTest extends Kohana_Unittest_TestCase +{ + protected $environmentDefault = array( + 'Kohana::$base_url' => '/kohana/', + 'HTTP_HOST' => 'www.kohanaframework.org', + ); + + /** + * Provides test data for test_attributes() + * + * @return array + */ + public function provider_attributes() + { + return array( + array( + array('name' => 'field', 'random' => 'not_quite', 'id' => 'unique_field'), + ' id="unique_field" name="field" random="not_quite"' + ), + array( + array('invalid' => NULL), + '' + ), + array( + array(), + '' + ) + ); + } + + /** + * Tests HTML::attributes() + * + * @test + * @dataProvider provider_attributes + * @param array $attributes Attributes to use + * @param string $expected Expected output + */ + public function test_attributes($attributes, $expected) + { + $this->assertSame( + $expected, + HTML::attributes($attributes) + ); + } + + /** + * Provides test data for test_script + * + * @return array Array of test data + */ + public function provider_script() + { + return array( + array( + '', + 'http://google.com/script.js', + ), + ); + } + + /** + * Tests HTML::script() + * + * @test + * @dataProvider provider_script + * @param string $expected Expected output + * @param string $file URL to script + * @param array $attributes HTML attributes for the anchor + * @param bool $index Should the index file be included in url? + */ + public function test_script($expected, $file, array $attributes = NULL, $index = FALSE) + { + $this->assertSame( + $expected, + HTML::script($file, $attributes, $index) + ); + } + + /** + * Data provider for the style test + * + * @return array Array of test data + */ + public function provider_style() + { + return array( + array( + '', + 'http://google.com/style.css', + array(), + FALSE + ), + ); + } + + /** + * Tests HTML::style() + * + * @test + * @dataProvider provider_style + * @param string $expected The expected output + * @param string $file The file to link to + * @param array $attributes Any extra attributes for the link + * @param bool $index Whether the index file should be added to the link + */ + public function test_style($expected, $file, array $attributes = NULL, $index = FALSE) + { + $this->assertSame( + $expected, + HTML::style($file, $attributes, $index) + ); + } + + /** + * Provides test data for test_obfuscate + * + * @return array Array of test data + */ + public function provider_obfuscate() + { + return array( + array('something crazy'), + array('me@google.com'), + ); + } + + /** + * Tests HTML::obfuscate + * + * @test + * @dataProvider provider_obfuscate + * @param string $string The string to obfuscate + */ + public function test_obfuscate($string) + { + $this->assertNotSame( + $string, + HTML::obfuscate($string) + ); + } + + /** + * Provides test data for test_anchor + * + * @return array Test data + */ + public function provider_anchor() + { + return array( + array( + 'Kohana', + array(), + 'http://kohanaframework.org', + 'Kohana', + ), + array( + 'GOOGLE', + array(), + 'http://google.com', + 'GOOGLE', + array('target' => '_blank'), + ), + ); + } + + /** + * Tests HTML::anchor + * + * @test + * @dataProvider provider_anchor + */ + public function test_anchor($expected, array $options, $uri, $title = NULL, array $attributes = NULL, $protocol = NULL) + { + //$this->setEnvironment($options); + + $this->assertSame( + $expected, + HTML::anchor($uri, $title, $attributes, $protocol) + ); + } + + /** + * Data provider for test_file_anchor + * + * @return array + */ + public function provider_file_anchor() + { + return array( + array( + 'My picture file', + array(), + 'mypic.png', + 'My picture file', + ) + ); + } + + /** + * Test for HTML::file_anchor() + * + * @test + * @covers HTML::file_anchor + * @dataProvider provider_file_anchor + */ + public function test_file_anchor($expected, array $options, $file, $title = NULL, array $attributes = NULL, $protocol = NULL) + { + $this->assertSame( + $expected, + HTML::file_anchor($file, $title, $attributes, $protocol) + ); + } +} diff --git a/includes/kohana/system/tests/kohana/I18nTest.php b/includes/kohana/system/tests/kohana/I18nTest.php new file mode 100644 index 00000000..d44bc213 --- /dev/null +++ b/includes/kohana/system/tests/kohana/I18nTest.php @@ -0,0 +1,73 @@ + + * @copyright (c) 2008-2010 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_I18nTest extends Kohana_Unittest_TestCase +{ + /** + * Provides test data for test_lang() + * + * @return array + */ + public function provider_lang() + { + return array( + // $value, $result + array(NULL, 'en-us'), + array('es-es', 'es-es'), + array(NULL, 'es-es'), + ); + } + + /** + * Tests i18n::lang() + * + * @test + * @dataProvider provider_lang + * @param boolean $input Input for File::mime + * @param boolean $expected Output for File::mime + */ + public function test_open($input, $expected) + { + $this->assertSame($expected, I18n::lang($input)); + } + + /** + * Provides test data for test_get() + * + * @return array + */ + public function provider_get() + { + return array( + // $value, $result + array('en-us', 'Hello, world!', 'Hello, world!'), + array('es-es', 'Hello, world!', '¡Hola, mundo!'), + array('fr-fr', 'Hello, world!', 'Bonjour, monde!'), + ); + } + + /** + * Tests i18n::get() + * + * @test + * @dataProvider provider_get + * @param boolean $input Input for File::mime + * @param boolean $expected Output for File::mime + */ + public function test_get($lang, $input, $expected) + { + I18n::lang($lang); + $this->assertSame($expected, I18n::get($input)); + } +} diff --git a/includes/kohana/system/tests/kohana/InflectorTest.php b/includes/kohana/system/tests/kohana/InflectorTest.php new file mode 100644 index 00000000..9a4d36cf --- /dev/null +++ b/includes/kohana/system/tests/kohana/InflectorTest.php @@ -0,0 +1,142 @@ + + * @copyright (c) 2008-2010 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_InflectorTest extends Kohana_Unittest_TestCase +{ + /** + * Provides test data for test_lang() + * + * @return array + */ + public function provider_uncountable() + { + return array( + // $value, $result + array('fish', TRUE), + array('cat', FALSE), + ); + } + + /** + * Tests Inflector::uncountable + * + * @test + * @dataProvider provider_uncountable + * @param boolean $input Input for File::mime + * @param boolean $expected Output for File::mime + */ + public function test_uncountable($input, $expected) + { + $this->assertSame($expected, Inflector::uncountable($input)); + } + + /** + * Provides test data for test_lang() + * + * @return array + */ + public function provider_singular() + { + return array( + // $value, $result + array('fish', NULL, 'fish'), + array('cats', NULL, 'cat'), + array('cats', 2, 'cats'), + array('cats', '2', 'cats'), + array('children', NULL, 'child'), + array('meters', 0.6, 'meters'), + array('meters', 1.6, 'meters'), + array('meters', 1.0, 'meter'), + array('status', NULL, 'status'), + array('statuses', NULL, 'status'), + ); + } + + /** + * Tests Inflector::singular + * + * @test + * @dataProvider provider_singular + * @param boolean $input Input for File::mime + * @param boolean $expected Output for File::mime + */ + public function test_singular($input, $count, $expected) + { + $this->assertSame($expected, Inflector::singular($input, $count)); + } + + /** + * Provides test data for test_lang() + * + * @return array + */ + public function provider_plural() + { + return array( + // $value, $result + array('fish', NULL, 'fish'), + array('cat', NULL, 'cats'), + array('cats', 1, 'cats'), + array('cats', '1', 'cats'), + array('movie', NULL, 'movies'), + array('meter', 0.6, 'meters'), + array('meter', 1.6, 'meters'), + array('meter', 1.0, 'meter'), + ); + } + + /** + * Tests Inflector::plural + * + * @test + * @dataProvider provider_plural + * @param boolean $input Input for File::mime + * @param boolean $expected Output for File::mime + */ + public function test_plural($input, $count, $expected) + { + $this->assertSame($expected, Inflector::plural($input, $count)); + } + + /** + * Provides test data for test_camelize() + * + * @return array + */ + public function provider_camelize() + { + return array( + // $value, $result + array('mother cat', 'camelize', 'motherCat'), + array('kittens in bed', 'camelize', 'kittensInBed'), + array('mother cat', 'underscore', 'mother_cat'), + array('kittens in bed', 'underscore', 'kittens_in_bed'), + array('kittens-are-cats', 'humanize', 'kittens are cats'), + array('dogs_as_well', 'humanize', 'dogs as well'), + ); + } + + /** + * Tests Inflector::camelize + * + * @test + * @dataProvider provider_camelize + * @param boolean $input Input for File::mime + * @param boolean $expected Output for File::mime + */ + public function test_camelize($input, $method, $expected) + { + $this->assertSame($expected, Inflector::$method($input)); + } +} diff --git a/includes/kohana/system/tests/kohana/LogTest.php b/includes/kohana/system/tests/kohana/LogTest.php new file mode 100644 index 00000000..9067e920 --- /dev/null +++ b/includes/kohana/system/tests/kohana/LogTest.php @@ -0,0 +1,87 @@ + + * @copyright (c) 2008-2010 Kohana Team + * @license http://kohanaphp.com/license + */ +Class Kohana_LogTest extends Kohana_Unittest_TestCase +{ + + /** + * Tests that when a new logger is created the list of messages is initially + * empty + * + * @test + * @covers Kohana_Log + */ + public function test_messages_is_initially_empty() + { + $logger = new Kohana_Log; + + $this->assertAttributeSame(array(), '_messages', $logger); + } + + /** + * Tests that when a new logger is created the list of writers is initially + * empty + * + * @test + * @covers Kohana_Log + */ + public function test_writers_is_initially_empty() + { + $logger = new Kohana_Log; + + $this->assertAttributeSame(array(), '_writers', $logger); + } + + /** + * Test that attaching a log writer adds it to the array of log writers + * + * @TODO Is this test too specific? + * + * @test + * @covers Kohana_Log::attach + */ + public function test_attach_attaches_log_writer_and_returns_this() + { + $logger = new Kohana_Log; + $writer = $this->getMockForAbstractClass('Kohana_Log_Writer'); + + $this->assertSame($logger, $logger->attach($writer)); + + $this->assertAttributeSame( + array(spl_object_hash($writer) => array('object' => $writer, 'types' => NULL)), + '_writers', + $logger + ); + } + + /** + * When we call detach() we expect the specified log writer to be removed + * + * @test + * @covers Kohana_Log::detach + */ + public function test_detach_removes_log_writer_and_returns_this() + { + $logger = new Kohana_Log; + $writer = $this->getMockForAbstractClass('Kohana_Log_Writer'); + + $logger->attach($writer); + + $this->assertSame($logger, $logger->detach($writer)); + + $this->assertAttributeSame(array(), '_writers', $logger); + } + + +} diff --git a/includes/kohana/system/tests/kohana/ModelTest.php b/includes/kohana/system/tests/kohana/ModelTest.php new file mode 100644 index 00000000..332fd831 --- /dev/null +++ b/includes/kohana/system/tests/kohana/ModelTest.php @@ -0,0 +1,30 @@ + + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_ModelTest extends Kohana_Unittest_TestCase +{ + /** + * @test + */ + public function test_construct() + { + #$model = new Model_Foobar('foo'); + #$model = Model::factory('Foobar', 'foo'); + } +} + +class Model_Foobar extends Model +{ + +} diff --git a/includes/kohana/system/tests/kohana/NumTest.php b/includes/kohana/system/tests/kohana/NumTest.php new file mode 100644 index 00000000..2031dc0c --- /dev/null +++ b/includes/kohana/system/tests/kohana/NumTest.php @@ -0,0 +1,95 @@ + + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +Class Kohana_NumTest extends Kohana_Unittest_TestCase +{ + protected $default_locale; + + /** + * SetUp test enviroment + */ + public function setUp() + { + parent::setUp(); + + setlocale(LC_ALL, 'English'); + } + + /** + * Tear down environment + */ + public function tearDown() + { + parent::tearDown(); + + setlocale(LC_ALL, $this->default_locale); + } + + /** + * Provides test data for test_ordinal() + * @return array + */ + public function provider_ordinal() + { + return array( + array(0, 'th'), + array(1, 'st'), + array(21, 'st'), + array(112, 'th'), + array(23, 'rd'), + array(42, 'nd'), + ); + } + + /** + * + * @test + * @dataProvider provider_ordinal + * @param integer $number + * @param $expected + */ + public function test_ordinal($number, $expected) + { + $this->assertSame($expected, Num::ordinal($number)); + } + + /** + * Provides test data for test_format() + * @return array + */ + public function provider_format() + { + return array( + // English + array(10000, 2, FALSE, '10,000.00'), + array(10000, 2, TRUE, '10,000.00'), + + // Additional dp's should be removed + array(123.456, 2, FALSE, '123.46'), + array(123.456, 2, TRUE, '123.46'), + ); + } + + /** + * @todo test locales + * @test + * @dataProvider provider_format + * @param integer $number + * @param integer $places + * @param boolean $monetary + * @param string $expected + */ + public function test_format($number, $places, $monetary, $expected) + { + $this->assertSame($expected, Num::format($number, $places, $monetary)); + } +} diff --git a/includes/kohana/system/tests/kohana/RemoteTest.php b/includes/kohana/system/tests/kohana/RemoteTest.php new file mode 100644 index 00000000..b29e0645 --- /dev/null +++ b/includes/kohana/system/tests/kohana/RemoteTest.php @@ -0,0 +1,77 @@ + + * @copyright (c) 2008-2010 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_RemoteTest extends Kohana_Unittest_TestCase +{ + /** + * Provides test data for test_get() + * + * @return array + */ + public function provider_get() + { + return array( + // $value, $result + array('', TRUE), + array('cat', FALSE), + ); + } + + /** + * Tests Remote::get + * + * @test + * @dataProvider provider_get + * @param boolean $input Input for File::mime + * @param boolean $expected Output for File::mime + */ + public function test_get($input, $expected) + { + if ( ! $this->hasInternet()) + $this->markTestSkipped('An internet connection is required for this test'); + + #$this->assertSame($expected, Remote::get($input)); + } + + /** + * Provides test data for test_status() + * + * @return array + */ + public function provider_status() + { + return array( + // $value, $result + array('http://kohanaframework.org/', 200), + array('http://kohanaframework.org', 200), + array('http://kohanaframework.org/foobar', 500), + ); + } + + /** + * Tests Remote::status + * + * @test + * @dataProvider provider_status + * @param boolean $input Input for File::mime + * @param boolean $expected Output for File::mime + */ + public function test_status($input, $expected) + { + if ( ! $this->hasInternet()) + $this->markTestSkipped('An internet connection is required for this test'); + + $this->assertSame($expected, Remote::status($input)); + } +} diff --git a/includes/kohana/system/tests/kohana/RequestTest.php b/includes/kohana/system/tests/kohana/RequestTest.php new file mode 100644 index 00000000..201b53c9 --- /dev/null +++ b/includes/kohana/system/tests/kohana/RequestTest.php @@ -0,0 +1,191 @@ + + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_RequestTest extends Kohana_Unittest_TestCase +{ + /** + * Route::matches() should return false if the route doesn't match against a uri + * + * @test + */ + public function test_create() + { + $request = Request::factory('foo/bar')->execute(); + + $this->assertEquals(200, $request->status); + $this->assertEquals('foo', $request->response); + + try + { + $request = new Request('bar/foo'); + $request->execute(); + } + catch (Exception $e) + { + $this->assertEquals(TRUE, $e instanceof ReflectionException); + $this->assertEquals('404', $request->status); + } + } + + /** + * Tests Request::accept_type() + * + * @test + * @covers Request::accept_type + */ + public function test_accept_type() + { + $this->assertEquals(array('*/*' => 1), Request::accept_type()); + } + + /** + * Provides test data for test_instance() + * + * @return array + */ + public function provider_instance() + { + return array( + // $route, $is_cli, $_server, $status, $response + array('foo/bar', TRUE, array(), 200, ''), // Shouldn't this be 'foo' ? + array('foo/foo', TRUE, array(), 200, ''), // Shouldn't this be a 404? + array( + 'foo/bar', + FALSE, + array( + 'REQUEST_METHOD' => 'get', + 'HTTP_REFERER' => 'http://www.kohanaframework.org', + 'HTTP_USER_AGENT' => 'Kohana Unit Test', + 'REMOTE_ADDR' => '127.0.0.1', + ), 200, ''), // Shouldn't this be 'foo' ? + ); + } + + /** + * Tests Request::instance() + * + * @test + * @dataProvider provider_instance + * @covers Request::instance + * @param boolean $value Input for Kohana::sanitize + * @param boolean $result Output for Kohana::sanitize + */ + public function test_instance($route, $is_cli, $server, $status, $response) + { + $this->setEnvironment(array( + '_SERVER' => $server+array('argc' => $_SERVER['argc']), + 'Kohana::$is_cli' => $is_cli, + 'Request::$instance' => NULL + )); + + $request = Request::instance($route); + + $this->assertEquals($status, $request->status); + $this->assertEquals($response, $request->response); + $this->assertEquals($route, $request->uri); + + if ( ! $is_cli) + { + $this->assertEquals($server['REQUEST_METHOD'], Request::$method); + $this->assertEquals($server['HTTP_REFERER'], Request::$referrer); + $this->assertEquals($server['HTTP_USER_AGENT'], Request::$user_agent); + } + } + + /** + * Provides test data for Request::accept_lang() + * @return array + */ + public function provider_accept_lang() + { + return array( + array('en-us', 1, array('_SERVER' => array('HTTP_ACCEPT_LANGUAGE' => 'en-us,en;q=0.5'))), + array('en-us', 1, array('_SERVER' => array('HTTP_ACCEPT_LANGUAGE' => 'en-gb'))), + array('en-us', 1, array('_SERVER' => array('HTTP_ACCEPT_LANGUAGE' => 'sp-sp;q=0.5'))) + ); + } + + /** + * Tests Request::accept_lang() + * + * @test + * @covers Request::accept_lang + * @dataProvider provider_accept_lang + * @param array $params Query string + * @param string $expected Expected result + * @param array $enviroment Set environment + */ + public function test_accept_lang($params, $expected, $enviroment) + { + $this->setEnvironment($enviroment); + + $this->assertEquals( + $expected, + Request::accept_lang($params) + ); + } + + /** + * Provides test data for Request::url() + * @return array + */ + public function provider_url() + { + return array( + array( + 'foo/bar', + array(), + 'http', + TRUE, + 'http://localhost/kohana/foo/bar' + ), + array( + 'foo', + array('action' => 'bar'), + 'http', + TRUE, + 'http://localhost/kohana/foo/bar' + ), + ); + } + + /** + * Tests Request::url() + * + * @test + * @dataProvider provider_url + * @covers Request::url + * @param string $route the route to use + * @param array $params params to pass to route::uri + * @param string $protocol the protocol to use + * @param array $expected The string we expect + */ + public function test_url($uri, $params, $protocol, $is_cli, $expected) + { + $this->setEnvironment(array( + 'Kohana::$base_url' => '/kohana/', + '_SERVER' => array('HTTP_HOST' => 'localhost', 'argc' => $_SERVER['argc']), + 'Kohana::$index_file' => FALSE, + 'Kohana::$is_cli' => $is_cli, + )); + + $this->assertEquals(Request::instance($uri)->url($params, $protocol), $expected); + } +} + +class Controller_Foo extends Controller { + public function action_bar() + { + $this->request->response = 'foo'; + } +} diff --git a/includes/kohana/system/tests/kohana/RouteTest.php b/includes/kohana/system/tests/kohana/RouteTest.php new file mode 100644 index 00000000..eb811541 --- /dev/null +++ b/includes/kohana/system/tests/kohana/RouteTest.php @@ -0,0 +1,416 @@ + + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_RouteTest extends Kohana_Unittest_TestCase +{ + /** + * Remove all caches + */ + public function setUp() + { + parent::setUp(); + + $this->cleanCacheDir(); + } + + /** + * Removes cache files created during tests + */ + public function tearDown() + { + parent::tearDown(); + + $this->cleanCacheDir(); + } + + /** + * If Route::get() is asked for a route that does not exist then + * it should throw a Kohana_Exception + * + * Note use of @expectedException + * + * @test + * @covers Route::get + * @expectedException Kohana_Exception + */ + public function test_get_throws_exception_if_route_dnx() + { + Route::get('HAHAHAHAHAHAHAHAHA'); + } + + /** + * Route::all() should return all routes defined via Route::set() + * and not through new Route() + * + * @test + * @covers Route::all + */ + public function test_all_returns_all_defined_routes() + { + $defined_routes = self::readAttribute('Route', '_routes'); + + $this->assertSame($defined_routes, Route::all()); + } + + /** + * Route::name() should fetch the name of a passed route + * If route is not found then it should return FALSE + * + * @TODO: This test needs to segregate the Route::$_routes singleton + * @test + * @covers Route::name + */ + public function test_name_returns_routes_name_or_false_if_dnx() + { + $route = Route::set('flamingo_people', 'flamingo/dance'); + + $this->assertSame('flamingo_people', Route::name($route)); + + $route = new Route('dance/dance'); + + $this->assertFalse(Route::name($route)); + } + + /** + * If Route::cache() was able to restore routes from the cache then + * it should return TRUE and load the cached routes + * + * @test + * @covers Route::cache + */ + public function test_cache_stores_route_objects() + { + $routes = Route::all(); + + // First we create the cache + Route::cache(TRUE); + + // Now lets modify the "current" routes + Route::set('nonsensical_route', 'flabbadaga/ding_dong'); + + // Then try and load said cache + $this->assertTrue(Route::cache()); + + // And if all went ok the nonsensical route should be gone... + $this->assertEquals($routes, Route::all()); + } + + /** + * Route::cache() should return FALSE if cached routes could not be found + * + * The cache is cleared before and after each test in setUp tearDown + * by cleanCacheDir() + * + * @test + * @covers Route::cache + */ + public function test_cache_returns_false_if_cache_dnx() + { + $this->assertSame(FALSE, Route::cache(), 'Route cache was not empty'); + } + + /** + * If the constructor is passed a NULL uri then it should assume it's + * being loaded from the cache & therefore shouldn't override the cached attributes + * + * @test + * @covers Route::__construct + */ + public function test_constructor_returns_if_uri_is_null() + { + // We use a mock object to make sure that the route wasn't recompiled + $route = $this->getMock('Route', array('_compile'), array(), '', FALSE); + + $route + ->expects($this->never()) + ->method('_compile'); + + $route->__construct(NULL,NULL); + + $this->assertAttributeSame('', '_uri', $route); + $this->assertAttributeSame(array(), '_regex', $route); + $this->assertAttributeSame(array('action' => 'index'), '_defaults', $route); + $this->assertAttributeSame(NULL, '_route_regex', $route); + } + + /** + * The constructor should only use custom regex if passed a non-empty array + * + * Technically we can't "test" this as the default regex is an empty array, this + * is purely for improving test coverage + * + * @test + * @covers Route::__construct + */ + public function test_constructor_only_changes_custom_regex_if_passed() + { + $route = new Route('/', array()); + + $this->assertAttributeSame(array(), '_regex', $route); + + $route = new Route('/', NULL); + + $this->assertAttributeSame(array(), '_regex', $route); + } + + /** + * When we pass custom regex to the route's constructor it should it + * in leu of the default + * + * @test + * @covers Route::__construct + * @covers Route::_compile + */ + public function test_route_uses_custom_regex_passed_to_constructor() + { + $regex = array('id' => '[0-9]{1,2}'); + + $route = new Route('(/(/))', $regex); + + $this->assertAttributeSame($regex, '_regex', $route); + $this->assertAttributeContains( + $regex['id'], + '_route_regex', + $route + ); + } + + /** + * Route::matches() should return false if the route doesn't match against a uri + * + * @test + * @covers Route::matches + */ + public function test_matches_returns_false_on_failure() + { + $route = new Route('projects/(/((/(/))))'); + + $this->assertSame(FALSE, $route->matches('apple/pie')); + } + + /** + * Route::matches() should return an array of parameters when a match is made + * An parameters that are not matched should not be present in the array of matches + * + * @test + * @covers Route::matches + */ + public function test_matches_returns_array_of_parameters_on_successful_match() + { + $route = new Route('((/(/)))'); + + $matches = $route->matches('welcome/index'); + + $this->assertType('array', $matches); + $this->assertArrayHasKey('controller', $matches); + $this->assertArrayHasKey('action', $matches); + $this->assertArrayNotHasKey('id', $matches); + $this->assertSame(2, count($matches)); + $this->assertSame('welcome', $matches['controller']); + $this->assertSame('index', $matches['action']); + } + + /** + * Defaults specified with defaults() should be used if their values aren't + * present in the uri + * + * @test + * @covers Route::matches + */ + public function test_defaults_are_used_if_params_arent_specified() + { + $route = new Route('((/(/)))'); + $route->defaults(array('controller' => 'welcome', 'action' => 'index')); + + $matches = $route->matches(''); + + $this->assertType('array', $matches); + $this->assertArrayHasKey('controller', $matches); + $this->assertArrayHasKey('action', $matches); + $this->assertArrayNotHasKey('id', $matches); + $this->assertSame(2, count($matches)); + $this->assertSame('welcome', $matches['controller']); + $this->assertSame('index', $matches['action']); + $this->assertSame('unit/test/1', $route->uri(array( + 'controller' => 'unit', + 'action' => 'test', + 'id' => '1' + ))); + $this->assertSame('welcome/index', $route->uri()); + } + + /** + * This tests that routes with required parameters will not match uris without them present + * + * @test + * @covers Route::matches + */ + public function test_required_parameters_are_needed() + { + $route = new Route('admin(/(/(/)))'); + + $this->assertFalse($route->matches('')); + + $matches = $route->matches('admin'); + + $this->assertType('array', $matches); + + $matches = $route->matches('admin/users/add'); + + $this->assertType('array', $matches); + $this->assertSame(2, count($matches)); + $this->assertArrayHasKey('controller', $matches); + $this->assertArrayHasKey('action', $matches); + } + + /** + * This tests the reverse routing returns the uri specified in the route + * if it's a static route + * + * A static route is a route without any parameters + * + * @test + * @covers Route::uri + */ + public function test_reverse_routing_returns_routes_uri_if_route_is_static() + { + $route = new Route('info/about_us'); + + $this->assertSame('info/about_us', $route->uri(array('some' => 'random', 'params' => 'to confuse'))); + } + + /** + * When Route::uri is working on a uri that requires certain parameters to be present + * (i.e. in '(/uri(array('action' => 'awesome-action')); + + $this->fail('Route::uri should throw exception if required param is not provided'); + } + catch(Exception $e) + { + $this->assertType('Kohana_Exception', $e); + // Check that the error in question is about the controller param + $this->assertContains('controller', $e->getMessage()); + } + } + + /** + * The logic for replacing required segments is separate (but similar) to that for + * replacing optional segments. + * + * This test asserts that Route::uri will replace required segments with provided + * params + * + * @test + * @covers Route::uri + */ + public function test_uri_fills_required_uri_segments_from_params() + { + $route = new Route('/(/)'); + + $this->assertSame( + 'users/edit', + $route->uri(array( + 'controller' => 'users', + 'action' => 'edit', + )) + ); + + $this->assertSame( + 'users/edit/god', + $route->uri(array( + 'controller' => 'users', + 'action' => 'edit', + 'id' => 'god', + )) + ); + } + + /** + * Provides test data for test_composing_url_from_route() + * @return array + */ + public function provider_composing_url_from_route() + { + return array( + array('/welcome'), + array('/news/view/42', array('controller' => 'news', 'action' => 'view', 'id' => 42)), + array('http://kohanaframework.org/news', array('controller' => 'news'), true) + ); + } + + /** + * Tests Route::url() + * + * Checks the url composing from specific route via Route::url() shortcut + * + * @test + * @dataProvider provider_composing_url_from_route + * @param string $expected + * @param array $params + * @param boolean $protocol + */ + public function test_composing_url_from_route($expected, $params = NULL, $protocol = NULL) + { + Route::set('foobar', '((/(/)))') + ->defaults(array( + 'controller' => 'welcome', + ) + ); + + $this->setEnvironment(array( + '_SERVER' => array('HTTP_HOST' => 'kohanaframework.org'), + 'Kohana::$base_url' => '/', + 'Request::$protocol' => 'http', + 'Kohana::$index_file' => '', + )); + + $this->assertSame($expected, Route::url('foobar', $params, $protocol)); + } + + /** + * Tests Route::_compile() + * + * Makes sure that compile will use custom regex if specified + * + * @test + * @covers Route::_compile + */ + public function test_compile_uses_custom_regex_if_specificed() + { + $route = new Route( + '(/(/))', + array( + 'controller' => '[a-z]+', + 'id' => '\d+', + ) + ); + + $this->assertAttributeSame( + '#^(?P[a-z]+)(?:/(?P[^/.,;?\n]++)(?:/(?P\d+))?)?$#uD', + '_route_regex', + $route + ); + } +} diff --git a/includes/kohana/system/tests/kohana/SecurityTest.php b/includes/kohana/system/tests/kohana/SecurityTest.php new file mode 100644 index 00000000..830aa1a8 --- /dev/null +++ b/includes/kohana/system/tests/kohana/SecurityTest.php @@ -0,0 +1,105 @@ +"), + ); + } + + /** + * Tests Security::encode_php_tags() + * + * @test + * @dataProvider provider_encode_php_tags + * @covers Security::encode_php_tags + */ + public function test_encode_php_tags($expected, $input) + { + $this->assertSame($expected, Security::encode_php_tags($input)); + } + + /** + * Provides test data for test_strip_image_tags() + * + * @return array Test data sets + */ + public function provider_strip_image_tags() + { + return array( + array('foo', ''), + ); + } + + /** + * Tests Security::strip_image_tags() + * + * @test + * @dataProvider provider_strip_image_tags + * @covers Security::strip_image_tags + */ + public function test_strip_image_tags($expected, $input) + { + $this->assertSame($expected, Security::strip_image_tags($input)); + } + + /** + * Provides test data for Security::token() + * + * @return array Test data sets + */ + public function provider_csrf_token() + { + $array = array(); + for ($i = 0; $i <= 4; $i++) + { + Security::$token_name = 'token_'.$i; + $array[] = array(Security::token(TRUE), Security::check(Security::token(FALSE)), $i); + } + return $array; + } + + /** + * Tests Security::token() + * + * @test + * @dataProvider provider_csrf_token + * @covers Security::token + */ + public function test_csrf_token($expected, $input, $iteration) + { + Security::$token_name = 'token_'.$iteration; + $this->assertSame(TRUE, $input); + $this->assertSame($expected, Security::token(FALSE)); + Session::instance()->delete(Security::$token_name); + } + + /** + * Tests that Security::xss_clean() removes null bytes + * + * + * @test + * @covers Security::xss_clean + * @ticket 2676 + * @see http://www.hakipedia.com/index.php/Poison_Null_Byte#Perl_PHP_Null_Byte_Injection + */ + public function test_xss_clean_removes_null_bytes() + { + $input = "<\0script>alert('XSS');<\0/script>"; + + $this->assertSame("alert('XSS');", Security::xss_clean($input)); + } +} diff --git a/includes/kohana/system/tests/kohana/SessionTest.php b/includes/kohana/system/tests/kohana/SessionTest.php new file mode 100644 index 00000000..59a7f95b --- /dev/null +++ b/includes/kohana/system/tests/kohana/SessionTest.php @@ -0,0 +1,497 @@ + + * @copyright (c) 2008-2010 Kohana Team + * @license http://kohanaphp.com/license + */ +Class Kohana_SessionTest extends Kohana_Unittest_TestCase +{ + + /** + * Gets a mock of the session class + * + * @return Session + */ + public function getMockSession(array $config = array()) + { + return $this->getMockForAbstractClass('Session', array($config)); + } + + /** + * Provides test data for + * + * test_constructor_uses_name_from_config_and_casts() + * + * @return array + */ + public function provider_constructor_uses_settings_from_config_and_casts() + { + return array( + // array(expected, input) + // data set 0 + array( + array( + 'name' => 'awesomeness', + 'lifetime' => 1231456421, + 'encrypted' => FALSE + ), + array( + 'name' => 'awesomeness', + 'lifetime' => '1231456421', + 'encrypted' => FALSE, + ), + ), + // data set 1 + array( + array( + 'name' => '123', + 'encrypted' => 'default', + ), + array( + 'name' => 123, + 'encrypted' => TRUE, + ), + ), + ); + } + + /** + * The constructor should change its attributes based on config + * passed as the first parameter + * + * @test + * @dataProvider provider_constructor_uses_settings_from_config_and_casts + * @covers Session::__construct + */ + public function test_constructor_uses_settings_from_config_and_casts($expected, $config) + { + $session = $this->getMockForAbstractClass('Session', array($config)); + + foreach($expected as $var => $value) + { + $this->assertAttributeSame($value, '_'.$var, $session); + } + } + + /** + * Check that the constructor will load a session if it's provided + * witha session id + * + * @test + * @covers Session::__construct + * @covers Session::read + */ + public function test_constructor_loads_session_with_session_id() + { + $this->markTestIncomplete( + 'Need to work out why constructor is not being called' + ); + + $config = array(); + $session_id = 'lolums'; + + // Don't auto-call constructor, we need to setup the mock first + $session = $this->getMockForAbstractClass( + 'Session', + array(), + '', + FALSE + ); + + $session + ->expects($this->once()) + ->method('read') + ->with($session_id); + + $session->__construct($config, $session_id); + } + + /** + * Calling $session->bind() should allow you to bind a variable + * to a session variable + * + * @test + * @covers Session::bind + * @ticket 3164 + */ + public function test_bind_actually_binds_variable() + { + $session = $this->getMockForAbstractClass('Session'); + + $var = 'asd'; + + $session->bind('our_var', $var); + + $var = 'foobar'; + + $this->assertSame('foobar', $session->get('our_var')); + } + + + /** + * When a session is initially created it should have no data + * + * + * @test + * @covers Session::__construct + * @covers Session::set + */ + public function test_initially_session_has_no_data() + { + $session = $this->getMockSession(); + + $this->assertAttributeSame(array(), '_data', $session); + } + + /** + * Make sure that the default session name (the one used if the + * driver does not set one) is 'session' + * + * @test + * @covers Session::__construct + */ + public function test_default_session_name_is_set() + { + $session = $this->getMockSession(); + + $this->assertAttributeSame('session', '_name', $session); + } + + /** + * By default sessions are unencrypted + * + * @test + * @covers Session::__construct + */ + public function test_default_session_is_unencrypted() + { + $session = $this->getMockSession(); + + $this->assertAttributeSame(FALSE, '_encrypted', $session); + } + + /** + * A new session should not be classed as destroyed + * + * @test + * @covers Session::__construct + */ + public function test_default_session_is_not_classed_as_destroyed() + { + $session = $this->getMockSession(); + + $this->assertAttributeSame(FALSE, '_destroyed', $session); + } + + /** + * Provides test data for test_get_returns_default_if_var_dnx() + * + * @return array + */ + public function provider_get_returns_default_if_var_dnx() + { + return array( + array('something_crazy', FALSE), + array('a_true', TRUE), + array('an_int', 158163158), + ); + } + + /** + * Make sure that get() is using the default value we provide and + * isn't tampering with it + * + * @test + * @dataProvider provider_get_returns_default_if_var_dnx + * @covers Session::get + */ + public function test_get_returns_default_if_var_dnx($var, $default) + { + $session = $this->getMockSession(); + + $this->assertSame($default, $session->get($var, $default)); + } + + /** + * By default get() should be using null as the var DNX return value + * + * @test + * @covers Session::get + */ + public function test_get_uses_null_as_default_return_value() + { + $session = $this->getMockSession(); + + $this->assertSame(NULL, $session->get('level_of_cool')); + } + + /** + * This test makes sure that session is using array_key_exists + * as isset will return FALSE if the value is NULL + * + * @test + * @covers Session::get + */ + public function test_get_returns_value_if_it_equals_null() + { + $session = $this->getMockSession(); + + $session->set('arkward', NULL); + + $this->assertSame(NULL, $session->get('arkward', 'uh oh')); + } + + /** + * as_array() should return the session data by reference. + * + * i.e. if we modify the returned data, the session data also changes + * + * @test + * @covers Session::as_array + */ + public function test_as_array_returns_data_by_ref_or_copy() + { + $session = $this->getMockSession(); + + $data_ref =& $session->as_array(); + + $data_ref['something'] = 'pie'; + + $this->assertAttributeSame($data_ref, '_data', $session); + + $data_copy = $session->as_array(); + + $data_copy['pie'] = 'awesome'; + + $this->assertAttributeNotSame($data_copy, '_data', $session); + } + + /** + * set() should add new session data and modify existing ones + * + * Also makes sure that set() returns $this + * + * @test + * @covers Session::set + */ + public function test_set_adds_and_modifies_to_session_data() + { + $session = $this->getMockSession(); + + $this->assertSame($session, $session->set('pork', 'pie')); + + $this->assertAttributeSame( + array('pork' => 'pie'), + '_data', + $session + ); + + $session->set('pork', 'delicious'); + + $this->assertAttributeSame( + array('pork' => 'delicious'), + '_data', + $session + ); + } + + /** + * This tests that delete() removes specified session data + * + * @test + * @covers Session::delete + */ + public function test_delete_removes_select_session_data() + { + $session = $this->getMockSession(); + + // Bit of a hack for mass-loading session data + $data =& $session->as_array(); + + $data += array( + 'a' => 'A', + 'b' => 'B', + 'c' => 'C', + 'easy' => '123' + ); + + // Make a copy of $data for testing purposes + $copy = $data; + + // First we make sure we can delete one item + // Also, check that delete returns $this + $this->assertSame($session, $session->delete('a')); + + unset($copy['a']); + + // We could test against $data but then we'd be testing + // that as_array() is returning by ref + $this->assertAttributeSame($copy, '_data', $session); + + // Now we make sure we can delete multiple items + // We're checking $this is returned just in case + $this->assertSame($session, $session->delete('b', 'c')); + unset($copy['b'], $copy['c']); + + $this->assertAttributeSame($copy, '_data', $session); + } + + /** + * Provides test data for test_read_loads_session_data() + * + * @return array + */ + public function provider_read_loads_session_data() + { + return array( + // If driver returns array then just load it up + array( + array(), + 'wacka_wacka', + array() + ), + array( + array('the it' => 'crowd'), + 'the_it_crowd', + array('the it' => 'crowd'), + ), + // If it's a string an encrpytion is disabled (by default) base64decode and unserialize + array( + array('dead' => 'arrival'), + 'lolums', + 'YToxOntzOjQ6ImRlYWQiO3M6NzoiYXJyaXZhbCI7fQ==' + ), + ); + } + + /** + * This is one of the "big" tests for the session lib + * + * The test makes sure that + * + * 1. Session asks the driver for the data relating to $session_id + * 2. That it will load the returned data into the session + * + * @test + * @dataProvider provider_read_loads_session_data + * @covers Session::read + */ + public function test_read_loads_session_data($expected_data, $session_id, $driver_data, array $config = array()) + { + $session = $this->getMockSession($config); + + $session->expects($this->once()) + ->method('_read') + ->with($session_id) + ->will($this->returnValue($driver_data)); + + $session->read($session_id); + $this->assertAttributeSame($expected_data, '_data', $session); + } + + /** + * regenerate() should tell the driver to regenerate its id + * + * @test + * @covers Session::regenerate + */ + public function test_regenerate_tells_driver_to_regenerate() + { + $session = $this->getMockSession(); + + $new_session_id = 'asdnoawdnoainf'; + + $session->expects($this->once()) + ->method('_regenerate') + ->with() + ->will($this->returnValue($new_session_id)); + + $this->assertSame($new_session_id, $session->regenerate()); + } + + /** + * If the driver destroys the session then all session data should be + * removed + * + * @test + * @covers Session::destroy + */ + public function test_destroy_deletes_data_if_driver_destroys_session() + { + $session = $this->getMockSession(); + + $session + ->set('asd', 'dsa') + ->set('dog', 'god'); + + $session + ->expects($this->once()) + ->method('_destroy') + ->with() + ->will($this->returnValue(TRUE)); + + $this->assertTrue($session->destroy()); + + $this->assertAttributeSame(array(), '_data', $session); + } + + /** + * The session data should only be deleted if the driver reports + * that the session was destroyed ok + * + * @test + * @covers Session::destroy + */ + public function test_destroy_only_deletes_data_if_driver_destroys_session() + { + $session = $this->getMockSession(); + + $session + ->set('asd', 'dsa') + ->set('dog', 'god'); + + $session + ->expects($this->once()) + ->method('_destroy') + ->with() + ->will($this->returnValue(FALSE)); + + $this->assertFalse($session->destroy()); + $this->assertAttributeSame( + array('asd' => 'dsa', 'dog' => 'god'), + '_data', + $session + ); + } + + /** + * If a session variable exists then get_once should get it then remove it. + * If the variable does not exist then it should return the default + * + * @test + * @covers Session::get_once + */ + public function test_get_once_gets_once_or_returns_default() + { + $session = $this->getMockSession(); + + $session->set('foo', 'bar'); + + // Test that a default is returned + $this->assertSame('mud', $session->get_once('fud', 'mud')); + + // Now test that it actually removes the value + $this->assertSame('bar', $session->get_once('foo')); + + $this->assertAttributeSame(array(), '_data', $session); + + $this->assertSame('maybe', $session->get_once('foo', 'maybe')); + } +} diff --git a/includes/kohana/system/tests/kohana/TextTest.php b/includes/kohana/system/tests/kohana/TextTest.php new file mode 100644 index 00000000..96786f47 --- /dev/null +++ b/includes/kohana/system/tests/kohana/TextTest.php @@ -0,0 +1,572 @@ +assertSame('', Text::auto_p('')); + } + + /** + * + * @return array Test Data + */ + public function provider_auto_para_does_not_enclose_html_tags_in_paragraphs() + { + return array( + array( + array('div'), + '
            Pick a plum of peppers
            ', + ), + array( + array('div'), + '
            Tangas
            ', + ), + ); + } + + /** + * This test makes sure that auto_p doesn't enclose HTML tags + * in paragraphs + * + * @test + * @dataProvider provider_auto_para_does_not_enclose_html_tags_in_paragraphs + */ + public function test_auto_para_does_not_enclose_html_tags_in_paragraphs(array $tags, $text) + { + $output = Text::auto_p($text); + + foreach($tags as $tag) + { + $this->assertNotTag( + array('tag' => $tag, 'ancestor' => array('tag' => 'p')), + $output + ); + } + } + + /** + * This test makes sure that auto_p surrounds a single line of text + * with paragraph tags + * + * @test + */ + public function test_auto_para_encloses_slot_in_paragraph() + { + $text = 'Pick a pinch of purple pepper'; + + $this->assertSame('

            '.$text.'

            ', Text::auto_p($text)); + } + + /** + * Data provider for test_limit_words + * + * @return array Array of test data + */ + public function provider_limit_words() + { + return array + ( + array('', '', 100, NULL), + array('…', 'The rain in spain', -10, NULL), + array('The rain…', 'The rain in spain', 2, NULL), + array('The rain...', 'The rain in spain', 2, '...'), + ); + } + + /** + * + * @test + * @dataProvider provider_limit_words + */ + public function test_limit_words($expected, $str, $limit, $end_char) + { + $this->assertSame($expected, Text::limit_words($str, $limit, $end_char)); + } + + /** + * Provides test data for test_limit_chars() + * + * @return array Test data + */ + public function provider_limit_chars() + { + return array + ( + array('', '', 100, NULL, FALSE), + array('…', 'BOO!', -42, NULL, FALSE), + array('making php bet…', 'making php better for the sane', 14, NULL, FALSE), + array('Garçon! Un café s.v.p.', 'Garçon! Un café s.v.p.', 50, '__', FALSE), + array('Garçon!__', 'Garçon! Un café s.v.p.', 8, '__', FALSE), + // @issue 3238 + array('making php…', 'making php better for the sane', 14, NULL, TRUE), + array('Garçon!__', 'Garçon! Un café s.v.p.', 9, '__', TRUE), + array('Garçon!__', 'Garçon! Un café s.v.p.', 7, '__', TRUE), + array('__', 'Garçon! Un café s.v.p.', 5, '__', TRUE), + ); + } + + /** + * Tests Text::limit_chars() + * + * @test + * @dataProvider provider_limit_chars + */ + public function test_limit_chars($expected, $str, $limit, $end_char, $preserve_words) + { + $this->assertSame($expected, Text::limit_chars($str, $limit, $end_char, $preserve_words)); + } + + /** + * Test Text::alternate() + * + * @test + */ + public function test_alternate_alternates_between_parameters() + { + list($val_a, $val_b, $val_c) = array('good', 'bad', 'ugly'); + + $this->assertSame('good', Text::alternate($val_a, $val_b, $val_c)); + $this->assertSame('bad', Text::alternate($val_a, $val_b, $val_c)); + $this->assertSame('ugly', Text::alternate($val_a, $val_b, $val_c)); + + $this->assertSame('good', Text::alternate($val_a, $val_b, $val_c)); + } + + /** + * Tests Text::alternate() + * + * @test + * @covers Text::alternate + */ + public function test_alternate_resets_when_called_with_no_params_and_returns_empty_string() + { + list($val_a, $val_b, $val_c) = array('yes', 'no', 'maybe'); + + $this->assertSame('yes', Text::alternate($val_a, $val_b, $val_c)); + + $this->assertSame('', Text::alternate()); + + $this->assertSame('yes', Text::alternate($val_a, $val_b, $val_c)); + } + + /** + * Provides test data for test_reducde_slashes() + * + * @returns array Array of test data + */ + public function provider_reduce_slashes() + { + return array + ( + array('/', '//'), + array('/google/php/kohana/', '//google/php//kohana//'), + ); + } + + /** + * Covers Text::reduce_slashes() + * + * @test + * @dataProvider provider_reduce_slashes + */ + public function test_reduce_slashes($expected, $str) + { + $this->assertSame($expected, Text::reduce_slashes($str)); + } + + /** + * Provides test data for test_censor() + * + * @return array Test data + */ + public function provider_censor() + { + + return array + ( + // If the replacement is 1 character long it should be repeated for the length of the removed word + array("A donkey is also an ***", 'A donkey is also an ass', array('ass'), '*', TRUE), + array("Cake### isn't nearly as good as kohana###", "CakePHP isn't nearly as good as kohanaphp", array('php'), '#', TRUE), + // If it's > 1 then it's just replaced straight out + array("If you're born out of wedlock you're a --expletive--", "If you're born out of wedlock you're a child", array('child'), '--expletive--', TRUE), + + array('class', 'class', array('ass'), '*', FALSE), + ); + } + + /** + * Tests Text::censor + * + * @test + * @dataProvider provider_censor + */ + public function test_censor($expected, $str, $badwords, $replacement, $replace_partial_words) + { + $this->assertSame($expected, Text::censor($str, $badwords, $replacement, $replace_partial_words)); + } + + /** + * Provides test data for test_random + * + * @return array Test Data + */ + public function provider_random() + { + return array( + array('alnum', 8), + array('alpha', 10), + array('hexdec', 20), + array('nozero', 5), + array('numeric', 14), + array('distinct', 12), + array('aeiou', 4), + ); + } + + /** + * Tests Text::random() as well as possible + * + * Obviously you can't compare a randomly generated string against a + * pre-generated one and check that they are the same as this goes + * against the whole ethos of random. + * + * This test just makes sure that the value returned is of the correct + * values and length + * + * @test + * @dataProvider provider_random + */ + public function test_random($type, $length) + { + $pool = (string) $type; + + switch ($pool) + { + case 'alnum': + $pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + break; + case 'alpha': + $pool = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + break; + case 'hexdec': + $pool = '0123456789abcdef'; + break; + case 'numeric': + $pool = '0123456789'; + break; + case 'nozero': + $pool = '123456789'; + break; + case 'distinct': + $pool = '2345679ACDEFHJKLMNPRSTUVWXYZ'; + break; + } + + $this->assertRegExp('/^['.$pool.']{'.$length.'}$/', Text::random($type, $length)); + } + + /** + * Provides test data for test_similar + * + * @return array + */ + public function provider_similar() + { + return array + ( + // TODO: add some more cases + array('foo', array('foobar', 'food', 'fooberry')), + ); + } + + /** + * Tests Text::similar() + * + * @test + * @dataProvider provider_similar + * @covers Text::similar + */ + public function test_similar($expected, $words) + { + $this->assertSame($expected, Text::similar($words)); + } + + /** + * Provides test data for test_bytes + * + * @return array + */ + public function provider_bytes() + { + return array + ( + // TODO: cover the other units + array('256.00 B', 256, NULL, NULL, TRUE), + array('1.02 kB', 1024, NULL, NULL, TRUE), + + // In case you need to know the size of a floppy disk in petabytes + array('0.00147 GB', 1.44 * 1000 * 1024, 'GB', '%01.5f %s', TRUE), + + // SI is the standard, but lets deviate slightly + array('1.00 MiB', 1024 * 1024, 'MiB', NULL, FALSE), + ); + } + + /** + * Tests Text::bytes() + * + * @test + * @dataProvider provider_bytes + */ + public function test_bytes($expected, $bytes, $force_unit, $format, $si) + { + $this->assertSame($expected, Text::bytes($bytes, $force_unit, $format, $si)); + } + + /** + * Provides test data for test_widont() + * + * @return array Test data + */ + public function provider_widont() + { + return array + ( + array('No gain, no pain', 'No gain, no pain'), + array("spaces?what'rethey?", "spaces?what'rethey?"), + array('', ''), + ); + } + + /** + * Tests Text::widont() + * + * @test + * @dataProvider provider_widont + */ + public function test_widont($expected, $string) + { + $this->assertSame($expected, Text::widont($string)); + } + + + /** + * This checks that auto_link_emails() respects word boundaries and does not + * just blindly replace all occurences of the email address in the text. + * + * In the sample below the algorithm was replacing all occurences of voorzitter@xxxx.com + * inc the copy in the second list item. + * + * It was updated in 6c199366efc1115545ba13108b876acc66c54b2d to respect word boundaries + * + * @test + * @covers Text::auto_link_emails + * @ticket 2772 + */ + public function test_auto_link_emails_respects_word_boundaries() + { + $original = '
              +
            • voorzitter@xxxx.com
            • +
            • vicevoorzitter@xxxx.com
            • +
            '; + + $this->assertFalse(strpos('vice', Text::auto_link_emails($original))); + } + + + /** + * Provides some test data for test_number() + * + * @return array + */ + public function provider_number() + { + return array( + array('one', 1), + array('twenty-three', 23), + array('fourty-two', 42), + array('five million, six hundred and thirty-two', 5000632), + array('five million, six hundred and thirty', 5000630), + array('nine hundred million', 900000000), + array('thirty-seven thousand', 37000), + array('one thousand and twenty-four', 1024), + ); + } + + /** + * Checks that Text::number formats a number into english text + * + * @test + * @dataProvider provider_number + */ + public function test_number($expected, $number) + { + $this->assertSame($expected, Text::number($number)); + } + + /** + * Provides test data for test_auto_link_urls() + * + * @return array + */ + public function provider_auto_link_urls() + { + return array( + // First we try with the really obvious url + array( + 'Some random text http://www.google.com', + 'Some random text http://www.google.com', + ), + // Then we try with varying urls + array( + 'Some random www.google.com', + 'Some random www.google.com', + ), + array( + 'Some random google.com', + 'Some random google.com', + ), + // Check that it doesn't link urls in a href + array( + 'Look at me Awesome stuff', + 'Look at me Awesome stuff', + ), + array( + 'Look at me http://www.google.com', + 'Look at me http://www.google.com', + ), + // @issue 3190 + array( + 'www.google.com', + 'www.google.com', + ), + array( + 'www.google.com http://www.google.com/', + 'www.google.com http://www.google.com/', + ), + ); + } + + /** + * Runs tests for Test::auto_link_urls + * + * @test + * @dataProvider provider_auto_link_urls + */ + public function test_auto_link_urls($expected, $text) + { + $this->assertSame($expected, Text::auto_link_urls($text)); + } + + /** + * Provides test data for test_auto_link_emails() + * + * @return array + */ + public function provider_auto_link_emails() + { + return array( + // @issue 3162 + array( + 'info@test.com', + 'info@test.com', + ), + array( + 'info@test.com', + 'info@test.com', + ), + // @issue 3189 + array( + 'email@address.com email@address.com', + 'email@address.com email@address.com', + ), + ); + } + + /** + * Runs tests for Test::auto_link_emails + * + * @test + * @dataProvider provider_auto_link_emails + */ + public function test_auto_link_emails($expected, $text) + { + // Use html_entity_decode because emails will be randomly encoded by HTML::mailto + $this->assertSame($expected, html_entity_decode(Text::auto_link_emails($text))); + } + + /** + * Provides test data for test_auto_link + * + * @return array Test data + */ + public function provider_auto_link() + { + return array( + array( + 'Hi there, my site is kohanaframework.org and you can email me at nobody@kohanaframework.org', + array('kohanaframework.org'), + ), + + array( + 'Hi my.domain.com@domain.com you came from', + FALSE, + array('my.domain.com@domain.com'), + ), + ); + } + + /** + * Tests Text::auto_link() + * + * @test + * @dataProvider provider_auto_link + */ + public function test_auto_link($text, $urls = array(), $emails = array()) + { + $linked_text = Text::auto_link($text); + + if($urls === FALSE) + { + $this->assertNotContains('http://', $linked_text); + } + elseif(count($urls)) + { + foreach($urls as $url) + { + // Assert that all the urls have been caught by text auto_link_urls() + $this->assertContains(Text::auto_link_urls($url), $linked_text); + } + } + + foreach($emails as $email) + { + $this->assertNotContains($email, $linked_text); + } + + } + +} diff --git a/includes/kohana/system/tests/kohana/URLTest.php b/includes/kohana/system/tests/kohana/URLTest.php new file mode 100644 index 00000000..33d60cbc --- /dev/null +++ b/includes/kohana/system/tests/kohana/URLTest.php @@ -0,0 +1,223 @@ + + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +Class Kohana_URLTest extends Kohana_Unittest_TestCase +{ + /** + * Default values for the environment, see setEnvironment + * @var array + */ + protected $environmentDefault = array( + 'Kohana::$base_url' => '/kohana/', + 'Kohana::$index_file'=> 'index.php', + 'Request::$protocol' => 'http', + 'HTTP_HOST' => 'example.com', + '_GET' => array(), + ); + + /** + * Provides test data for test_base() + * + * @return array + */ + public function provider_base() + { + return array( + // $index, $protocol, $expected, $enviroment + // + // Test with different combinations of parameters for max code coverage + array(FALSE, FALSE, '/kohana/'), + array(FALSE, TRUE, 'http://example.com/kohana/'), + array(TRUE, FALSE, '/kohana/index.php/'), + array(TRUE, FALSE, '/kohana/index.php/'), + array(TRUE, TRUE, 'http://example.com/kohana/index.php/'), + array(TRUE, 'http', 'http://example.com/kohana/index.php/'), + array(TRUE, 'https','https://example.com/kohana/index.php/'), + array(TRUE, 'ftp', 'ftp://example.com/kohana/index.php/'), + + // + // These tests make sure that the protocol changes when the global setting changes + array(TRUE, TRUE, 'https://example.com/kohana/index.php/', array('Request::$protocol' => 'https')), + array(FALSE, TRUE, 'https://example.com/kohana/', array('Request::$protocol' => 'https')), + + // Change base url' + array(FALSE, 'https', 'https://example.com/kohana/', array('Kohana::$base_url' => 'omglol://example.com/kohana/')), + + // Use protocol from base url if none specified + array(FALSE, FALSE, 'http://www.example.com/', array('Kohana::$base_url' => 'http://www.example.com/')), + + // Use HTTP_HOST before SERVER_NAME + array(FALSE, 'http', 'http://example.com/kohana/', array('HTTP_HOST' => 'example.com', 'SERVER_NAME' => 'example.org')), + + // Use SERVER_NAME if HTTP_HOST DNX + array(FALSE, 'http', 'http://example.org/kohana/', array('HTTP_HOST' => NULL, 'SERVER_NAME' => 'example.org')), + ); + } + + /** + * Tests URL::base() + * + * @test + * @dataProvider provider_base + * @param boolean $index Parameter for Url::base() + * @param boolean $protocol Parameter for Url::base() + * @param string $expected Expected url + * @param array $enviroment Array of enviroment vars to change @see Kohana_URLTest::setEnvironment() + */ + public function test_base($index, $protocol, $expected, array $enviroment = array()) + { + $this->setEnvironment($enviroment); + + $this->assertSame( + $expected, + URL::base($index, $protocol) + ); + } + + /** + * Provides test data for test_site() + * + * @return array + */ + public function provider_site() + { + return array( + array('', FALSE, '/kohana/index.php/'), + array('', TRUE, 'http://example.com/kohana/index.php/'), + + array('my/site', FALSE, '/kohana/index.php/my/site'), + array('my/site', TRUE, 'http://example.com/kohana/index.php/my/site'), + + // @ticket #3110 + array('my/site/page:5', FALSE, '/kohana/index.php/my/site/page:5'), + array('my/site/page:5', TRUE, 'http://example.com/kohana/index.php/my/site/page:5'), + + array('my/site?var=asd&kohana=awesome', FALSE, '/kohana/index.php/my/site?var=asd&kohana=awesome'), + array('my/site?var=asd&kohana=awesome', TRUE, 'http://example.com/kohana/index.php/my/site?var=asd&kohana=awesome'), + + array('?kohana=awesome&life=good', FALSE, '/kohana/index.php/?kohana=awesome&life=good'), + array('?kohana=awesome&life=good', TRUE, 'http://example.com/kohana/index.php/?kohana=awesome&life=good'), + + array('?kohana=awesome&life=good#fact', FALSE, '/kohana/index.php/?kohana=awesome&life=good#fact'), + array('?kohana=awesome&life=good#fact', TRUE, 'http://example.com/kohana/index.php/?kohana=awesome&life=good#fact'), + + array('some/long/route/goes/here?kohana=awesome&life=good#fact', FALSE, '/kohana/index.php/some/long/route/goes/here?kohana=awesome&life=good#fact'), + array('some/long/route/goes/here?kohana=awesome&life=good#fact', TRUE, 'http://example.com/kohana/index.php/some/long/route/goes/here?kohana=awesome&life=good#fact'), + + array('/route/goes/here?kohana=awesome&life=good#fact', 'https', 'https://example.com/kohana/index.php/route/goes/here?kohana=awesome&life=good#fact'), + array('/route/goes/here?kohana=awesome&life=good#fact', 'ftp', 'ftp://example.com/kohana/index.php/route/goes/here?kohana=awesome&life=good#fact'), + ); + } + + /** + * Tests URL::site() + * + * @test + * @dataProvider provider_site + * @param string $uri URI to use + * @param boolean|string $protocol Protocol to use + * @param string $expected Expected result + * @param array $enviroment Array of enviroment vars to set + */ + public function test_site($uri, $protocol, $expected, array $enviroment = array()) + { + $this->setEnvironment($enviroment); + + $this->assertSame( + $expected, + URL::site($uri, $protocol) + ); + } + + /** + * Provides test data for test_title() + * @return array + */ + public function provider_title() + { + return array( + // Tests that.. + // Title is converted to lowercase + array('we-shall-not-be-moved', 'WE SHALL NOT BE MOVED', '-'), + // Excessive white space is removed and replaced with 1 char + array('thissssss-is-it', 'THISSSSSS IS IT ', '-'), + // separator is either - (dash) or _ (underscore) & others are converted to underscores + array('some-title', 'some title', '-'), + array('some_title', 'some title', '_'), + array('some!title', 'some title', '!'), + array('some:title', 'some title', ':'), + // Numbers are preserved + array('99-ways-to-beat-apple', '99 Ways to beat apple', '-'), + // ... with lots of spaces & caps + array('99_ways_to_beat_apple', '99 ways TO beat APPLE', '_'), + array('99-ways-to-beat-apple', '99 ways TO beat APPLE', '-'), + // Invalid characters are removed + array('each-gbp-is-now-worth-32-usd', 'Each GBP(£) is now worth 32 USD($)', '-'), + // ... inc. separator + array('is-it-reusable-or-re-usable', 'Is it reusable or re-usable?', '-'), + // Doing some crazy UTF8 tests + array('espana-wins', 'España-wins', '-', TRUE), + ); + } + + /** + * Tests URL::title() + * + * @test + * @dataProvider provider_title + * @param string $title Input to convert + * @param string $separator Seperate to replace invalid characters with + * @param string $expected Expected result + */ + public function test_Title($expected, $title, $separator, $ascii_only = FALSE) + { + $this->assertSame( + $expected, + URL::title($title, $separator, $ascii_only) + ); + } + + /** + * Provides test data for URL::query() + * @return array + */ + public function provider_Query() + { + return array( + array(NULL, '', array()), + array(NULL, '?test=data', array('_GET' => array('test' => 'data'))), + array(array('test' => 'data'), '?test=data', array()), + array(array('test' => 'data'), '?more=data&test=data', array('_GET' => array('more' => 'data'))) + ); + } + + /** + * Tests URL::query() + * + * @test + * @dataProvider provider_query + * @param array $params Query string + * @param string $expected Expected result + * @param array $enviroment Set environment + */ + public function test_query($params, $expected, $enviroment) + { + $this->setEnvironment($enviroment); + + $this->assertSame( + $expected, + URL::query($params) + ); + } +} diff --git a/includes/kohana/system/tests/kohana/UTF8Test.php b/includes/kohana/system/tests/kohana/UTF8Test.php new file mode 100644 index 00000000..0c280599 --- /dev/null +++ b/includes/kohana/system/tests/kohana/UTF8Test.php @@ -0,0 +1,156 @@ +assertSame($expected, UTF8::clean($input)); + } + + /** + * Provides test data for test_is_ascii() + */ + public function provider_is_ascii() + { + return array( + array("\0", TRUE), + array("\$eno\r", TRUE), + array('Señor', FALSE), + array(array('Se', 'nor'), TRUE), + array(array('Se', 'ñor'), FALSE), + ); + } + + /** + * Tests UTF8::is_ascii + * + * @test + * @dataProvider provider_is_ascii + */ + public function test_is_ascii($input, $expected) + { + $this->assertSame($expected, UTF8::is_ascii($input)); + } + + /** + * Provides test data for test_strip_ascii_ctrl() + */ + public function provider_strip_ascii_ctrl() + { + return array( + array("\0", ''), + array("→foo\021", '→foo'), + array("\x7Fbar", 'bar'), + array("\xFF", "\xFF"), + array("\x41", 'A'), + ); + } + + /** + * Tests UTF8::strip_ascii_ctrl + * + * @test + * @dataProvider provider_strip_ascii_ctrl + */ + public function test_strip_ascii_ctrl($input, $expected) + { + $this->assertSame($expected, UTF8::strip_ascii_ctrl($input)); + } + + /** + * Provides test data for test_strip_non_ascii() + */ + public function provider_strip_non_ascii() + { + return array( + array("\0\021\x7F", "\0\021\x7F"), + array('I ♥ cocoñùт', 'I coco'), + ); + } + + /** + * Tests UTF8::strip_non_ascii + * + * @test + * @dataProvider provider_strip_non_ascii + */ + public function test_strip_non_ascii($input, $expected) + { + $this->assertSame($expected, UTF8::strip_non_ascii($input)); + } + + /** + * Provides test data for test_str_ireplace() + */ + public function provider_str_ireplace() + { + return array( + array('т', 't', 'cocoñuт', 'cocoñut'), + array('Ñ', 'N', 'cocoñuт', 'cocoNuт'), + array(array('т', 'Ñ'), array('t', 'N'), 'cocoñuт', 'cocoNut'), + ); + } + + /** + * Tests UTF8::str_ireplace + * + * @test + * @dataProvider provider_str_ireplace + */ + public function test_str_ireplace($search, $replace, $subject, $expected) + { + $this->assertSame($expected, UTF8::str_ireplace($search, $replace, $subject)); + } + + /** + * Provides test data for test_strip_non_ascii() + */ + public function provider_ucwords() + { + return array( + array('ExAmple', 'ExAmple'), + array('i ♥ Cocoñùт', 'I ♥ Cocoñùт'), + ); + } + + /** + * Tests UTF8::ucwords + * + * @test + * @dataProvider provider_ucwords + */ + public function test_ucwords($input, $expected) + { + $this->assertSame($expected, UTF8::ucwords($input)); + } +} \ No newline at end of file diff --git a/includes/kohana/system/tests/kohana/UploadTest.php b/includes/kohana/system/tests/kohana/UploadTest.php new file mode 100644 index 00000000..2184c372 --- /dev/null +++ b/includes/kohana/system/tests/kohana/UploadTest.php @@ -0,0 +1,223 @@ + + * @copyright (c) 2008-2010 Kohana Team + * @license http://kohanaphp.com/license + */ +class Kohana_UploadTest extends Kohana_Unittest_TestCase +{ + /** + * Provides test data for test_size() + * + * @return array + */ + public function provider_size() + { + return array( + // $field, $bytes, $environment, $expected + array( + 'unit_test', + 5, + array('_FILES' => array('unit_test' => array('error' => UPLOAD_ERR_INI_SIZE))), + FALSE + ), + array( + 'unit_test', + 5, + array('_FILES' => array('unit_test' => array('error' => UPLOAD_ERR_NO_FILE))), + TRUE + ), + array( + 'unit_test', + '6K', + array('_FILES' => array( + 'unit_test' => array( + 'error' => UPLOAD_ERR_OK, + 'name' => 'Unit_Test File', + 'type' => 'image/png', + 'tmp_name' => Kohana::find_file('tests', 'test_data/github', 'png'), + 'size' => filesize(Kohana::find_file('tests', 'test_data/github', 'png')), + ) + ) + ), + TRUE + ), + array( + 'unit_test', + '1B', + array('_FILES' => array( + 'unit_test' => array( + 'error' => UPLOAD_ERR_OK, + 'name' => 'Unit_Test File', + 'type' => 'image/png', + 'tmp_name' => Kohana::find_file('tests', 'test_data/github', 'png'), + 'size' => filesize(Kohana::find_file('tests', 'test_data/github', 'png')), + ) + ) + ), + FALSE + ), + ); + } + + /** + * Tests Upload::size + * + * @test + * @dataProvider provider_size + * @covers upload::size + * @param string $field the files field to test + * @param string $bytes valid bite size + * @param array $environment set the $_FILES array + * @param $expected what to expect + */ + public function test_size($field, $bytes, $environment, $expected) + { + $this->setEnvironment($environment); + + $this->assertSame($expected, Upload::size($_FILES[$field], $bytes)); + } + + /** + * size() should throw an exception of the supplied max size is invalid + * + * @test + * @covers upload::size + * @expectedException Kohana_Exception + */ + public function test_size_throws_exception_for_invalid_size() + { + $this->setEnvironment(array( + '_FILES' => array( + 'unit_test' => array( + 'error' => UPLOAD_ERR_OK, + 'name' => 'Unit_Test File', + 'type' => 'image/png', + 'tmp_name' => Kohana::find_file('tests', 'test_data/github', 'png'), + 'size' => filesize(Kohana::find_file('tests', 'test_data/github', 'png')), + ) + ) + )); + + Upload::size($_FILES['unit_test'], '1DooDah'); + } + + /** + * Provides test data for test_vali() + * + * @test + * @return array + */ + public function provider_valid() + { + return array( + array( + TRUE, + array( + 'error' => UPLOAD_ERR_OK, + 'name' => 'Unit_Test File', + 'type' => 'image/png', + 'tmp_name' => Kohana::find_file('tests', 'test_data/github', 'png'), + 'size' => filesize(Kohana::find_file('tests', 'test_data/github', 'png')), + ) + ), + array( + FALSE, + array( + 'name' => 'Unit_Test File', + 'type' => 'image/png', + 'tmp_name' => Kohana::find_file('tests', 'test_data/github', 'png'), + 'size' => filesize(Kohana::find_file('tests', 'test_data/github', 'png')), + ) + ), + array( + FALSE, + array( + 'error' => UPLOAD_ERR_OK, + 'type' => 'image/png', + 'tmp_name' => Kohana::find_file('tests', 'test_data/github', 'png'), + 'size' => filesize(Kohana::find_file('tests', 'test_data/github', 'png')), + ) + ), + array( + FALSE, + array( + 'name' => 'Unit_Test File', + 'error' => UPLOAD_ERR_OK, + 'tmp_name' => Kohana::find_file('tests', 'test_data/github', 'png'), + 'size' => filesize(Kohana::find_file('tests', 'test_data/github', 'png')), + ) + ), + array( + FALSE, + array( + 'error' => UPLOAD_ERR_OK, + 'name' => 'Unit_Test File', + 'type' => 'image/png', + 'size' => filesize(Kohana::find_file('tests', 'test_data/github', 'png')), + ) + ), + array( + FALSE, + array( + 'error' => UPLOAD_ERR_OK, + 'name' => 'Unit_Test File', + 'type' => 'image/png', + 'tmp_name' => Kohana::find_file('tests', 'test_data/github', 'png'), + ) + ), + + ); + } + + /** + * Test Upload::valid + * + * @test + * @dataProvider provider_valid + * @covers Upload::valid + */ + public function test_valid($expected, $file) + { + $this->setEnvironment(array( + '_FILES' => array( + 'unit_test' => $file, + ), + )); + + $this->assertSame($expected, Upload::valid($_FILES['unit_test'])); + } + + /** + * Tests Upload::type + * + * @test + * @covers Upload::type + */ + public function test_type() + { + $this->setEnvironment(array( + '_FILES' => array( + 'unit_test' => array( + 'error' => UPLOAD_ERR_OK, + 'name' => 'github.png', + 'type' => 'image/png', + 'tmp_name' => Kohana::find_file('tests', 'test_data/github', 'png'), + 'size' => filesize(Kohana::find_file('tests', 'test_data/github', 'png')), + ) + ) + )); + + $this->assertTrue(Upload::type($_FILES['unit_test'], array('jpg', 'png', 'gif'))); + + $this->assertFalse(Upload::type($_FILES['unit_test'], array('docx'))); + } +} diff --git a/includes/kohana/system/tests/kohana/ValidateTest.php b/includes/kohana/system/tests/kohana/ValidateTest.php new file mode 100644 index 00000000..80d5d3be --- /dev/null +++ b/includes/kohana/system/tests/kohana/ValidateTest.php @@ -0,0 +1,1274 @@ + + * @copyright (c) 2008-2009 Kohana Team + * @license http://kohanaphp.com/license + */ +Class Kohana_ValidateTest extends Kohana_Unittest_TestCase +{ + + /** + * Provides test data for test_alpha() + * @return array + */ + public function provider_alpha() + { + return array( + array('asdavafaiwnoabwiubafpowf', TRUE), + array('!aidhfawiodb', FALSE), + array('51535oniubawdawd78', FALSE), + array('!"£$(G$W£(HFW£F(HQ)"n', FALSE), + // UTF-8 tests + array('あいうえお', TRUE, TRUE), + array('¥', FALSE, TRUE) + ); + } + + /** + * Tests Validate::alpha() + * + * Checks whether a string consists of alphabetical characters only. + * + * @test + * @group kohana.validation.helpers + * @dataProvider provider_alpha + * @param string $string + * @param boolean $expected + */ + public function test_alpha($string, $expected, $utf8 = FALSE) + { + $this->assertSame( + $expected, + Validate::alpha($string, $utf8) + ); + } + + /* + * Provides test data for test_alpha_numeric + */ + public function provide_alpha_numeric() + { + return array( + array('abcd1234', TRUE), + array('abcd', TRUE), + array('1234', TRUE), + array('abc123&^/-', FALSE), + // UTF-8 tests + array('あいうえお', TRUE, TRUE), + array('零一二三四五', TRUE, TRUE), + array('あい四五£^£^', FALSE, TRUE), + ); + } + + /** + * Tests Validate::alpha_numberic() + * + * Checks whether a string consists of alphabetical characters and numbers only. + * + * @test + * @group kohana.validation.helpers + * @dataProvider provide_alpha_numeric + * @param string $input The string to test + * @param boolean $expected Is $input valid + */ + public function test_alpha_numeric($input, $expected, $utf8 = FALSE) + { + $this->assertSame( + $expected, + Validate::alpha_numeric($input, $utf8) + ); + } + + /** + * Provides test data for test_alpha_dash + */ + public function provider_alpha_dash() + { + return array( + array('abcdef', TRUE), + array('12345', TRUE), + array('abcd1234', TRUE), + array('abcd1234-', TRUE), + array('abc123&^/-', FALSE) + ); + } + + /** + * Tests Validate::alpha_dash() + * + * Checks whether a string consists of alphabetical characters, numbers, underscores and dashes only. + * + * @test + * @group kohana.validation.helpers + * @dataProvider provider_alpha_dash + * @param string $input The string to test + * @param boolean $contains_utf8 Does the string contain utf8 specific characters + * @param boolean $expected Is $input valid? + */ + public function test_alpha_dash($input, $expected, $contains_utf8 = FALSE) + { + if( ! $contains_utf8) + { + $this->assertSame( + $expected, + Validate::alpha_dash($input) + ); + } + + $this->assertSame( + $expected, + Validate::alpha_dash($input, TRUE) + ); + } + + /** + * DataProvider for the valid::date() test + */ + public function provider_date() + { + return array( + array('now',TRUE), + array('10 September 2010',TRUE), + array('+1 day',TRUE), + array('+1 week',TRUE), + array('+1 week 2 days 4 hours 2 seconds',TRUE), + array('next Thursday',TRUE), + array('last Monday',TRUE), + + array('blarg',FALSE), + array('in the year 2000',FALSE), + array('324824',FALSE), + ); + } + + /** + * Tests Validate::date() + * + * @test + * @group kohana.validation.helpers + * @dataProvider provider_date + * @param string $date The date to validate + * @param integer $expected + */ + public function test_date($date, $expected) + { + $this->assertSame( + $expected, + Validate::date($date, $expected) + ); + } + + /** + * DataProvider for the valid::decimal() test + */ + public function provider_decimal() + { + return array( + array('45.1664', 3, NULL, FALSE), + array('45.1664', 4, NULL, TRUE), + array('45.1664', 4, 2, TRUE), + ); + } + + /** + * Tests Validate::decimal() + * + * @test + * @group kohana.validation.helpers + * @dataProvider provider_decimal + * @param string $decimal The decimal to validate + * @param integer $places The number of places to check to + * @param integer $digits The number of digits preceding the point to check + * @param boolean $expected Whether $decimal conforms to $places AND $digits + */ + public function test_decimal($decimal, $places, $digits, $expected) + { + $this->assertSame( + $expected, + Validate::decimal($decimal, $places, $digits), + 'Decimal: "'.$decimal.'" to '.$places.' places and '.$digits.' digits (preceeding period)' + ); + } + + /** + * Provides test data for test_digit + * @return array + */ + public function provider_digit() + { + return array( + array('12345', TRUE), + array('10.5', FALSE), + array('abcde', FALSE), + array('abcd1234', FALSE), + array('-5', FALSE), + array(-5, FALSE), + ); + } + + /** + * Tests Validate::digit() + * + * @test + * @group kohana.validation.helpers + * @dataProvider provider_digit + * @param mixed $input Input to validate + * @param boolean $expected Is $input valid + */ + public function test_digit($input, $expected, $contains_utf8 = FALSE) + { + if( ! $contains_utf8) + { + $this->assertSame( + $expected, + Validate::digit($input) + ); + } + + $this->assertSame( + $expected, + Validate::digit($input, TRUE) + ); + + } + + /** + * DataProvider for the valid::color() test + */ + public function provider_color() + { + return array( + array('#000000', TRUE), + array('#GGGGGG', FALSE), + array('#AbCdEf', TRUE), + array('#000', TRUE), + array('#abc', TRUE), + array('#DEF', TRUE), + array('000000', TRUE), + array('GGGGGG', FALSE), + array('AbCdEf', TRUE), + array('000', TRUE), + array('DEF', TRUE) + ); + } + + /** + * Tests Validate::color() + * + * @test + * @group kohana.validation.helpers + * @dataProvider provider_color + * @param string $color The color to test + * @param boolean $expected Is $color valid + */ + public function test_color($color, $expected) + { + $this->assertSame( + $expected, + Validate::color($color) + ); + } + + /** + * Provides test data for test_credit_card() + */ + public function provider_credit_card() + { + return array( + array('4222222222222', 'visa', TRUE), + array('4012888888881881', 'visa', TRUE), + array('4012888888881881', NULL, TRUE), + array('4012888888881881', array('mastercard', 'visa'), TRUE), + array('4012888888881881', array('discover', 'mastercard'), FALSE), + array('4012888888881881', 'mastercard', FALSE), + array('5105105105105100', 'mastercard', TRUE), + array('6011111111111117', 'discover', TRUE), + array('6011111111111117', 'visa', FALSE) + ); + } + + /** + * Tests Validate::credit_card() + * + * @test + * @covers Validate::credit_card + * @group kohana.validation.helpers + * @dataProvider provider_credit_card() + * @param string $number Credit card number + * @param string $type Credit card type + * @param boolean $expected + */ + public function test_credit_card($number, $type, $expected) + { + $this->assertSame( + $expected, + Validate::credit_card($number, $type) + ); + } + + /** + * Provides test data for test_credit_card() + */ + public function provider_luhn() + { + return array( + array('4222222222222', TRUE), + array('4012888888881881', TRUE), + array('5105105105105100', TRUE), + array('6011111111111117', TRUE), + array('60111111111111.7', FALSE), + array('6011111111111117X', FALSE), + array('6011111111111117 ', FALSE), + array('WORD ', FALSE), + ); + } + + /** + * Tests Validate::luhn() + * + * @test + * @covers Validate::luhn + * @group kohana.validation.helpers + * @dataProvider provider_luhn() + * @param string $number Credit card number + * @param boolean $expected + */ + public function test_luhn($number, $expected) + { + $this->assertSame( + $expected, + Validate::luhn($number) + ); + } + + /** + * Provides test data for test_email() + * + * @return array + */ + public function provider_email() + { + return array( + array('foo', TRUE, FALSE), + array('foo', FALSE, FALSE), + + // RFC is less strict than the normal regex, presumably to allow + // admin@localhost, therefore we IGNORE IT!!! + array('foo@bar', FALSE, FALSE), + array('foo@bar.com', FALSE, TRUE), + array('foo@bar.sub.com', FALSE, TRUE), + array('foo+asd@bar.sub.com', FALSE, TRUE), + array('foo.asd@bar.sub.com', FALSE, TRUE), + ); + } + + /** + * Tests Validate::email() + * + * Check an email address for correct format. + * + * @test + * @group kohana.validation.helpers + * @dataProvider provider_email + * @param string $email Address to check + * @param boolean $strict Use strict settings + * @param boolean $correct Is $email address valid? + */ + public function test_email($email, $strict, $correct) + { + $this->assertSame( + $correct, + Validate::email($email, $strict) + ); + } + + /** + * Returns test data for test_email_domain() + * + * @return array + */ + public function provider_email_domain() + { + return array( + array('google.com', TRUE), + // Don't anybody dare register this... + array('DAWOMAWIDAIWNDAIWNHDAWIHDAIWHDAIWOHDAIOHDAIWHD.com', FALSE) + ); + } + + /** + * Tests Validate::email_domain() + * + * Validate the domain of an email address by checking if the domain has a + * valid MX record. + * + * Test skips on windows + * + * @test + * @group kohana.validation.helpers + * @dataProvider provider_email_domain + * @param string $email Email domain to check + * @param boolean $correct Is it correct? + */ + public function test_email_domain($email, $correct) + { + if ( ! $this->hasInternet()) + $this->markTestSkipped('An internet connection is required for this test'); + + if( ! Kohana::$is_windows OR version_compare(PHP_VERSION, '5.3.0', '>=')) + { + $this->assertSame( + $correct, + Validate::email_domain($email) + ); + } + else + { + $this->markTestSkipped('checkdnsrr() was not added on windows until PHP 5.3'); + } + } + + /** + * Provides data for test_exact_length() + * + * @return array + */ + public function provider_exact_length() + { + return array( + array('somestring', 10, TRUE), + array('anotherstring', 13, TRUE), + ); + } + + /** + * + * Tests Validate::exact_length() + * + * Checks that a field is exactly the right length. + * + * @test + * @group kohana.validation.helpers + * @dataProvider provider_exact_length + * @param string $string The string to length check + * @param integer $length The length of the string + * @param boolean $correct Is $length the actual length of the string? + * @return bool + */ + public function test_exact_length($string, $length, $correct) + { + return $this->assertSame( + $correct, + Validate::exact_length($string, $length), + 'Reported string length is not correct' + ); + } + + /** + * Tests Validate::factory() + * + * Makes sure that the factory method returns an instance of Validate lib + * and that it uses the variables passed + * + * @test + */ + public function test_factory_method_returns_instance_with_values() + { + $values = array( + 'this' => 'something else', + 'writing tests' => 'sucks', + 'why the hell' => 'amIDoingThis', + ); + + $instance = Validate::factory($values); + + $this->assertTrue($instance instanceof Validate); + + $this->assertSame( + $values, + $instance->as_array() + ); + } + + /** + * DataProvider for the valid::ip() test + * @return array + */ + public function provider_ip() + { + return array( + array('75.125.175.50', FALSE, TRUE), + array('127.0.0.1', FALSE, TRUE), + array('256.257.258.259', FALSE, FALSE), + array('255.255.255.255', FALSE, FALSE), + array('192.168.0.1', FALSE, FALSE), + array('192.168.0.1', TRUE, TRUE) + ); + } + + /** + * Tests Validate::ip() + * + * @test + * @group kohana.validation.helpers + * @dataProvider provider_ip + * @param string $input_ip + * @param boolean $allow_private + * @param boolean $expected_result + */ + public function test_ip($input_ip, $allow_private, $expected_result) + { + $this->assertEquals( + $expected_result, + Validate::ip($input_ip, $allow_private) + ); + } + + /** + * Returns test data for test_max_length() + * + * @return array + */ + public function provider_max_length() + { + return array( + // Border line + array('some', 4, TRUE), + // Exceeds + array('KOHANARULLLES', 2, FALSE), + // Under + array('CakeSucks', 10, TRUE) + ); + } + + /** + * Tests Validate::max_length() + * + * Checks that a field is short enough. + * + * @test + * @group kohana.validation.helpers + * @dataProvider provider_max_length + * @param string $string String to test + * @param integer $maxlength Max length for this string + * @param boolean $correct Is $string <= $maxlength + */ + public function test_max_length($string, $maxlength, $correct) + { + $this->assertSame( + $correct, + Validate::max_length($string, $maxlength) + ); + } + + /** + * Returns test data for test_min_length() + * + * @return array + */ + public function provider_min_length() + { + return array( + array('This is obviously long enough', 10, TRUE), + array('This is not', 101, FALSE), + array('This is on the borderline', 25, TRUE) + ); + } + + /** + * Tests Validate::min_length() + * + * Checks that a field is long enough. + * + * @test + * @group kohana.validation.helpers + * @dataProvider provider_min_length + * @param string $string String to compare + * @param integer $minlength The minimum allowed length + * @param boolean $correct Is $string 's length >= $minlength + */ + public function test_min_length($string, $minlength, $correct) + { + $this->assertSame( + $correct, + Validate::min_length($string, $minlength) + ); + } + + /** + * Returns test data for test_not_empty() + * + * @return array + */ + public function provider_not_empty() + { + // Create a blank arrayObject + $ao = new ArrayObject; + + // arrayObject with value + $ao1 = new ArrayObject; + $ao1['test'] = 'value'; + + return array( + array(array(), FALSE), + array(NULL, FALSE), + array('', FALSE), + array($ao, FALSE), + array($ao1, TRUE), + array(array(NULL), TRUE), + array(0, TRUE), + array('0', TRUE), + array('Something', TRUE), + ); + } + + /** + * Tests Validate::not_empty() + * + * Checks if a field is not empty. + * + * @test + * @group kohana.validation.helpers + * @dataProvider provider_not_empty + * @param mixed $value Value to check + * @param boolean $empty Is the value really empty? + */ + public function test_not_empty($value, $empty) + { + return $this->assertSame( + $empty, + Validate::not_empty($value) + ); + } + + /** + * DataProvider for the Validate::numeric() test + */ + public function provider_numeric() + { + return array( + array(12345, TRUE), + array(123.45, TRUE), + array('12345', TRUE), + array('10.5', TRUE), + array('-10.5', TRUE), + array('10.5a', FALSE), + // @issue 3240 + array(.4, TRUE), + array(-.4, TRUE), + array(4., TRUE), + array(-4., TRUE), + array('.5', TRUE), + array('-.5', TRUE), + array('5.', TRUE), + array('-5.', TRUE), + array('.', FALSE), + array('1.2.3', FALSE), + ); + } + + /** + * Tests Validate::numeric() + * + * @test + * @group kohana.validation.helpers + * @dataProvider provider_numeric + * @param string $input Input to test + * @param boolean $expected Whether or not $input is numeric + */ + public function test_numeric($input, $expected) + { + $this->assertSame( + $expected, + Validate::numeric($input) + ); + } + + /** + * Provides test data for test_phone() + * @return array + */ + public function provider_phone() + { + return array( + array('0163634840', NULL, TRUE), + array('+27173634840', NULL, TRUE), + array('123578', NULL, FALSE), + // Some uk numbers + array('01234456778', NULL, TRUE), + array('+0441234456778', NULL, FALSE), + // Google UK case you're interested + array('+44 20-7031-3000', array(12), TRUE), + // BT Corporate + array('020 7356 5000', NULL, TRUE), + ); + } + + /** + * Tests Validate::phone() + * + * @test + * @group kohana.validation.helpers + * @dataProvider provider_phone + * @param string $phone Phone number to test + * @param boolean $expected Is $phone valid + */ + public function test_phone($phone, $lengths, $expected) + { + $this->assertSame( + $expected, + Validate::phone($phone, $lengths) + ); + } + + /** + * DataProvider for the valid::regex() test + */ + public function provider_regex() + { + return array( + array('hello world', '/[a-zA-Z\s]++/', TRUE), + array('123456789', '/[0-9]++/', TRUE), + array('£$%£%', '/[abc]/', FALSE), + array('Good evening', '/hello/', FALSE), + ); + } + + /** + * Tests Validate::range() + * + * Tests if a number is within a range. + * + * @test + * @group kohana.validation.helpers + * @dataProvider provider_regex + * @param string Value to test against + * @param string Valid pcre regular expression + * @param bool Does the value match the expression? + */ + public function test_regex($value, $regex, $expected) + { + $this->AssertSame( + $expected, + Validate::regex($value, $regex) + ); + } + + /** + * DataProvider for the valid::range() test + */ + public function provider_range() + { + return array( + array(1, 0, 2, TRUE), + array(-1, -5, 0, TRUE), + array(-1, 0, 1, FALSE), + array(1, 0, 0, FALSE), + array(2147483647, 0, 200000000000000, TRUE), + array(-2147483647, -2147483655, 2147483645, TRUE) + ); + } + + /** + * Tests Validate::range() + * + * Tests if a number is within a range. + * + * @test + * @group kohana.validation.helpers + * @dataProvider provider_range + * @param integer $number Number to test + * @param integer $min Lower bound + * @param integer $max Upper bound + * @param boolean $expected Is Number within the bounds of $min && $max + */ + public function test_range($number, $min, $max, $expected) + { + $this->AssertSame( + $expected, + Validate::range($number, $min, $max) + ); + } + + /** + * Provides test data for test_url() + * + * @return array + */ + public function provider_url() + { + $data = array( + array('http://google.com', TRUE), + array('http://google.com/', TRUE), + array('http://google.com/?q=abc', TRUE), + array('http://google.com/#hash', TRUE), + array('http://localhost', TRUE), + array('http://hello-world.pl', TRUE), + array('http://hello--world.pl', TRUE), + array('http://h.e.l.l.0.pl', TRUE), + array('http://server.tld/get/info', TRUE), + array('http://127.0.0.1', TRUE), + array('http://127.0.0.1:80', TRUE), + array('http://user@127.0.0.1', TRUE), + array('http://user:pass@127.0.0.1', TRUE), + array('ftp://my.server.com', TRUE), + array('rss+xml://rss.example.com', TRUE), + + array('http://google.2com', FALSE), + array('http://google.com?q=abc', FALSE), + array('http://google.com#hash', FALSE), + array('http://hello-.pl', FALSE), + array('http://hel.-lo.world.pl', FALSE), + array('http://ww£.google.com', FALSE), + array('http://127.0.0.1234', FALSE), + array('http://127.0.0.1.1', FALSE), + array('http://user:@127.0.0.1', FALSE), + array("http://finalnewline.com\n", FALSE), + ); + + $data[] = array('http://'.str_repeat('123456789.', 25).'com/', TRUE); // 253 chars + $data[] = array('http://'.str_repeat('123456789.', 25).'info/', FALSE); // 254 chars + + return $data; + } + + /** + * Tests Validate::url() + * + * @test + * @group kohana.validation.helpers + * @dataProvider provider_url + * @param string $url The url to test + * @param boolean $expected Is it valid? + */ + public function test_url($url, $expected) + { + $this->assertSame( + $expected, + Validate::url($url) + ); + } + + /** + * When we copy() a validate object, we should have a new validate object + * with the exact same attributes, apart from the data, which should be the + * same as the array we pass to copy() + * + * @test + * @covers Validate::copy + */ + public function test_copy_copies_all_attributes_except_data() + { + $validate = new Validate(array('foo' => 'bar', 'fud' => 'fear, uncertainty, doubt', 'num' => 9)); + + $validate->rule('num', 'is_int')->rule('foo', 'is_string'); + + $validate->callback('foo', 'heh', array('ding')); + + $copy_data = array('foo' => 'no', 'fud' => 'maybe', 'num' => 42); + + $copy = $validate->copy($copy_data); + + $this->assertNotSame($validate, $copy); + + foreach(array('_filters', '_rules', '_callbacks', '_labels', '_empty_rules', '_errors') as $attribute) + { + // This is just an easy way to check that the attributes are identical + // Without hardcoding the expected values + $this->assertAttributeSame( + self::readAttribute($validate, $attribute), + $attribute, + $copy + ); + } + + $this->assertSame($copy_data, $copy->as_array()); + } + + /** + * By default there should be no callbacks registered with validate + * + * @test + */ + public function test_initially_there_are_no_callbacks() + { + $validate = new Validate(array()); + + $this->assertAttributeSame(array(), '_callbacks', $validate); + } + + /** + * This is just a quick check that callback() returns a reference to $this + * + * @test + * @covers Validate::callback + */ + public function test_callback_returns_chainable_this() + { + $validate = new Validate(array()); + + $this->assertSame($validate, $validate->callback('field', 'something')); + } + + /** + * Check that callback() is storign callbacks in the correct manner + * + * @test + * @covers Validate::callback + */ + public function test_callback_stores_callback() + { + $validate = new Validate(array('id' => 355)); + + $validate->callback('id', 'misc_callback'); + + $this->assertAttributeSame( + array( + 'id' => array(array('misc_callback', array())), + ), + '_callbacks', + $validate + ); + } + + /** + * Calling Validate::callbacks() should store multiple callbacks for the specified field + * + * @test + * @covers Validate::callbacks + * @covers Validate::callback + */ + public function test_callbacks_stores_multiple_callbacks() + { + $validate = new Validate(array('year' => 1999)); + + $validate->callbacks('year', array('misc_callback', 'another_callback')); + + $this->assertAttributeSame( + array( + 'year' => array( + array('misc_callback', array()), + array('another_callback', array()), + ), + ), + '_callbacks', + $validate + ); + } + + /** + * When the validate object is initially created there should be no labels + * specified + * + * @test + */ + public function test_initially_there_are_no_labels() + { + $validate = new Validate(array()); + + $this->assertAttributeSame(array(), '_labels', $validate); + } + + /** + * Adding a label to a field should set it in the labels array + * If the label already exists it should overwrite it + * + * In both cases thefunction should return a reference to $this + * + * @test + * @covers Validate::label + */ + public function test_label_adds_and_overwrites_label_and_returns_this() + { + $validate = new Validate(array()); + + $this->assertSame($validate, $validate->label('email', 'Email Address')); + + $this->assertAttributeSame(array('email' => 'Email Address'), '_labels', $validate); + + $this->assertSame($validate, $validate->label('email', 'Your Email')); + + $validate->label('name', 'Your Name'); + + $this->assertAttributeSame( + array('email' => 'Your Email', 'name' => 'Your Name'), + '_labels', + $validate + ); + } + + /** + * Using labels() we should be able to add / overwrite multiple labels + * + * The function should also return $this for chaining purposes + * + * @test + * @covers Validate::labels + */ + public function test_labels_adds_and_overwrites_multiple_labels_and_returns_this() + { + $validate = new Validate(array()); + $initial_data = array('kung fu' => 'fighting', 'fast' => 'cheetah'); + + $this->assertSame($validate, $validate->labels($initial_data)); + + $this->assertAttributeSame($initial_data, '_labels', $validate); + + $this->assertSame($validate, $validate->labels(array('fast' => 'lightning'))); + + $this->assertAttributeSame( + array('fast' => 'lightning', 'kung fu' => 'fighting'), + '_labels', + $validate + ); + } + + /** + * We should be able to add a filter to the queue by calling filter() + * + * @test + * @covers Validate::filter + */ + public function test_filter_adds_a_filter_and_returns_this() + { + $validate = new Validate(array()); + + $this->assertSame($validate, $validate->filter('name', 'trim')); + + $this->assertAttributeSame( + array('name' => array('trim' => array())), + '_filters', + $validate + ); + } + + /** + * filters() should be able to add multiple filters for a field and return + * $this when done + * + * @test + * @covers Validate::filters + */ + public function test_filters_adds_multiple_filters_and_returns_this() + { + $validate = new Validate(array()); + + $this->assertSame( + $validate, + $validate->filters('id', array('trim' => NULL, 'some_func' => array('yes', 'no'))) + ); + + $this->assertAttributeSame( + array('id' => array('trim' => array(), 'some_func' => array('yes', 'no'))), + '_filters', + $validate + ); + } + + /** + * Provides test data for test_check + * + * @return array + */ + public function provider_check() + { + $mock = $this->getMock('Crazy_Test', array('unit_test_callback')); + // TODO: enchance this / make params more specific + $mock + ->expects($this->once()) + ->method('unit_test_callback') + ->withAnyParameters(); + + // $first_array, $second_array, $rules, $first_expected, $second_expected + return array( + array( + array('foo' => 'bar'), + array('foo' => array('not_empty', NULL)), + array('foo' => array($mock, 'unit_test_callback')), + TRUE, + array(), + ), + array( + array('unit' => 'test'), + array('foo' => array('not_empty', NULL), 'unit' => array('min_length', 6)), + array(), + FALSE, + array('foo' => 'foo must not be empty', 'unit' => 'unit must be at least 6 characters long'), + ), + ); + } + + /** + * Tests Validate::check() + * + * @test + * @covers Validate::check + * @covers Validate::callbacks + * @covers Validate::callback + * @covers Validate::rule + * @covers Validate::rules + * @covers Validate::errors + * @covers Validate::error + * @dataProvider provider_check + * @param string $url The url to test + * @param boolean $expected Is it valid? + */ + public function test_check($array, $rules, $callbacks, $expected, $expected_errors) + { + $validate = new Validate($array); + + foreach ($rules as $field => $rule) + $validate->rule($field, $rule[0], array($rule[1])); + foreach ($callbacks as $field => $callback) + $validate->callback($field, $callback); + + $status = $validate->check(); + $errors = $validate->errors(TRUE); + $this->assertSame($expected, $status); + $this->assertSame($expected_errors, $errors); + + $validate = new Validate($array); + foreach ($rules as $field => $rule) + $validate->rules($field, array($rule[0] => array($rule[1]))); + $this->assertSame($expected, $validate->check()); + } + + /** + * This test asserts that Validate::check will call callbacks with all of the + * parameters supplied when the callback was specified + * + * @test + * @covers Validate::callback + */ + public function test_object_callback_with_parameters() + { + $params = array(42, 'kohana' => 'rocks'); + + $validate = new Validate(array('foo' => 'bar')); + + // Generate an isolated callback + $mock = $this->getMock('Random_Class_That_DNX', array('unit_test_callback')); + + $mock->expects($this->once()) + ->method('unit_test_callback') + ->with($validate, 'foo', $params); + + $validate->callback('foo', array($mock, 'unit_test_callback'), $params); + + $validate->check(); + } + + /** + * In some cases (such as when validating search params in GET) it is necessary for + * an empty array to validate successfully + * + * This test checks that Validate::check() allows the user to specify this setting when + * calling check() + * + * @test + * @ticket 3059 + * @covers Validate::check + */ + public function test_check_allows_option_for_empty_data_array_to_validate() + { + $validate = new Validate(array()); + + $this->assertFalse($validate->check(FALSE)); + + $this->assertTrue($validate->check(TRUE)); + + $validate->rule('name', 'not_empty'); + + $this->assertFalse($validate->check(TRUE)); + $this->assertFalse($validate->check()); + } + + /** + * If you add a rule that says a field should match another field then + * a label should be added for the field to match against to ensure that + * it will be available when check() is called + * + * @test + * @ticket 3158 + * @covers Validate::rule + */ + public function test_rule_adds_label_if_rule_is_match_and_label_dnx() + { + $data = array('password' => 'lolcats', 'password_confirm' => 'lolcats'); + $labels = array('password' => 'password', 'password_confirm' => 'password confirm'); + + $validate = new Validate($data); + + $validate->rule('password', 'matches', array('password_confirm')); + + $this->assertAttributeSame($labels, '_labels', $validate); + + $this->assertTrue($validate->check()); + + // Now we do the dnx check + + $validate = new Validate($data); + + $labels = array('password_confirm' => 'TEH PASS') + $labels; + $validate->label('password_confirm', $labels['password_confirm']); + + $validate->rule('password', 'matches', array('password_confirm')); + + $this->assertAttributeSame($labels, '_labels', $validate); + + $this->assertTrue($validate->check()); + } + + /** + * Provides test data for test_errors() + * + * @return array + */ + public function provider_errors() + { + // [data, rules, expected], ... + return array( + array( + array('username' => 'frank'), + array('username' => array('not_empty' => NULL)), + array(), + ), + array( + array('username' => ''), + array('username' => array('not_empty' => NULL)), + array('username' => 'username must not be empty'), + ), + ); + } + + /** + * Tests Validate::errors() + * + * @test + * @covers Validate::errors + * @dataProvider provider_errors + * @param string $url The url to test + * @param boolean $expected Is it valid? + */ + public function test_errors($array, $rules, $expected) + { + $validate = Validate::factory($array); + + foreach($rules as $field => $field_rules) + { + $validate->rules($field, $field_rules); + } + + $validate->check(); + + $this->assertSame($expected, $validate->errors('validate', FALSE)); + } +} diff --git a/includes/kohana/system/tests/test_data/github.png b/includes/kohana/system/tests/test_data/github.png new file mode 100644 index 0000000000000000000000000000000000000000..81176849612fc7d246de8e4e03a68d51e1a90ca8 GIT binary patch literal 5101 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 literal 0 HcmV?d00001 diff --git a/includes/kohana/system/utf8/from_unicode.php b/includes/kohana/system/utf8/from_unicode.php new file mode 100644 index 00000000..ee3c336c --- /dev/null +++ b/includes/kohana/system/utf8/from_unicode.php @@ -0,0 +1,68 @@ += 0) AND ($arr[$k] <= 0x007f)) + { + echo chr($arr[$k]); + } + // 2 byte sequence + elseif ($arr[$k] <= 0x07ff) + { + echo chr(0xc0 | ($arr[$k] >> 6)); + echo chr(0x80 | ($arr[$k] & 0x003f)); + } + // Byte order mark (skip) + elseif ($arr[$k] == 0xFEFF) + { + // nop -- zap the BOM + } + // Test for illegal surrogates + elseif ($arr[$k] >= 0xD800 AND $arr[$k] <= 0xDFFF) + { + // Found a surrogate + trigger_error('UTF8::from_unicode: Illegal surrogate at index: '.$k.', value: '.$arr[$k], E_USER_WARNING); + return FALSE; + } + // 3 byte sequence + elseif ($arr[$k] <= 0xffff) + { + echo chr(0xe0 | ($arr[$k] >> 12)); + echo chr(0x80 | (($arr[$k] >> 6) & 0x003f)); + echo chr(0x80 | ($arr[$k] & 0x003f)); + } + // 4 byte sequence + elseif ($arr[$k] <= 0x10ffff) + { + echo chr(0xf0 | ($arr[$k] >> 18)); + echo chr(0x80 | (($arr[$k] >> 12) & 0x3f)); + echo chr(0x80 | (($arr[$k] >> 6) & 0x3f)); + echo chr(0x80 | ($arr[$k] & 0x3f)); + } + // Out of range + else + { + trigger_error('UTF8::from_unicode: Codepoint out of Unicode range at index: '.$k.', value: '.$arr[$k], E_USER_WARNING); + return FALSE; + } + } + + $result = ob_get_contents(); + ob_end_clean(); + return $result; +} diff --git a/includes/kohana/system/utf8/ltrim.php b/includes/kohana/system/utf8/ltrim.php new file mode 100644 index 00000000..8ee37241 --- /dev/null +++ b/includes/kohana/system/utf8/ltrim.php @@ -0,0 +1,22 @@ += 0 AND $ord0 <= 127) + return $ord0; + + if ( ! isset($chr[1])) + { + trigger_error('Short sequence - at least 2 bytes expected, only 1 seen', E_USER_WARNING); + return FALSE; + } + + $ord1 = ord($chr[1]); + + if ($ord0 >= 192 AND $ord0 <= 223) + return ($ord0 - 192) * 64 + ($ord1 - 128); + + if ( ! isset($chr[2])) + { + trigger_error('Short sequence - at least 3 bytes expected, only 2 seen', E_USER_WARNING); + return FALSE; + } + + $ord2 = ord($chr[2]); + + if ($ord0 >= 224 AND $ord0 <= 239) + return ($ord0 - 224) * 4096 + ($ord1 - 128) * 64 + ($ord2 - 128); + + if ( ! isset($chr[3])) + { + trigger_error('Short sequence - at least 4 bytes expected, only 3 seen', E_USER_WARNING); + return FALSE; + } + + $ord3 = ord($chr[3]); + + if ($ord0 >= 240 AND $ord0 <= 247) + return ($ord0 - 240) * 262144 + ($ord1 - 128) * 4096 + ($ord2-128) * 64 + ($ord3 - 128); + + if ( ! isset($chr[4])) + { + trigger_error('Short sequence - at least 5 bytes expected, only 4 seen', E_USER_WARNING); + return FALSE; + } + + $ord4 = ord($chr[4]); + + if ($ord0 >= 248 AND $ord0 <= 251) + return ($ord0 - 248) * 16777216 + ($ord1-128) * 262144 + ($ord2 - 128) * 4096 + ($ord3 - 128) * 64 + ($ord4 - 128); + + if ( ! isset($chr[5])) + { + trigger_error('Short sequence - at least 6 bytes expected, only 5 seen', E_USER_WARNING); + return FALSE; + } + + if ($ord0 >= 252 AND $ord0 <= 253) + return ($ord0 - 252) * 1073741824 + ($ord1 - 128) * 16777216 + ($ord2 - 128) * 262144 + ($ord3 - 128) * 4096 + ($ord4 - 128) * 64 + (ord($chr[5]) - 128); + + if ($ord0 >= 254 AND $ord0 <= 255) + { + trigger_error('Invalid UTF-8 with surrogate ordinal '.$ord0, E_USER_WARNING); + return FALSE; + } +} \ No newline at end of file diff --git a/includes/kohana/system/utf8/rtrim.php b/includes/kohana/system/utf8/rtrim.php new file mode 100644 index 00000000..1d76216d --- /dev/null +++ b/includes/kohana/system/utf8/rtrim.php @@ -0,0 +1,22 @@ + $val) + { + $str[$key] = UTF8::str_ireplace($search, $replace, $val, $count); + } + return $str; + } + + if (is_array($search)) + { + $keys = array_keys($search); + + foreach ($keys as $k) + { + if (is_array($replace)) + { + if (array_key_exists($k, $replace)) + { + $str = UTF8::str_ireplace($search[$k], $replace[$k], $str, $count); + } + else + { + $str = UTF8::str_ireplace($search[$k], '', $str, $count); + } + } + else + { + $str = UTF8::str_ireplace($search[$k], $replace, $str, $count); + } + } + return $str; + } + + $search = UTF8::strtolower($search); + $str_lower = UTF8::strtolower($str); + + $total_matched_strlen = 0; + $i = 0; + + while (preg_match('/(.*?)'.preg_quote($search, '/').'/s', $str_lower, $matches)) + { + $matched_strlen = strlen($matches[0]); + $str_lower = substr($str_lower, $matched_strlen); + + $offset = $total_matched_strlen + strlen($matches[1]) + ($i * (strlen($replace) - 1)); + $str = substr_replace($str, $replace, $offset, strlen($search)); + + $total_matched_strlen += $matched_strlen; + $i++; + } + + $count += $i; + return $str; +} diff --git a/includes/kohana/system/utf8/str_pad.php b/includes/kohana/system/utf8/str_pad.php new file mode 100644 index 00000000..3cff81c2 --- /dev/null +++ b/includes/kohana/system/utf8/str_pad.php @@ -0,0 +1,50 @@ +0x0061, 0x03A6=>0x03C6, 0x0162=>0x0163, 0x00C5=>0x00E5, 0x0042=>0x0062, + 0x0139=>0x013A, 0x00C1=>0x00E1, 0x0141=>0x0142, 0x038E=>0x03CD, 0x0100=>0x0101, + 0x0490=>0x0491, 0x0394=>0x03B4, 0x015A=>0x015B, 0x0044=>0x0064, 0x0393=>0x03B3, + 0x00D4=>0x00F4, 0x042A=>0x044A, 0x0419=>0x0439, 0x0112=>0x0113, 0x041C=>0x043C, + 0x015E=>0x015F, 0x0143=>0x0144, 0x00CE=>0x00EE, 0x040E=>0x045E, 0x042F=>0x044F, + 0x039A=>0x03BA, 0x0154=>0x0155, 0x0049=>0x0069, 0x0053=>0x0073, 0x1E1E=>0x1E1F, + 0x0134=>0x0135, 0x0427=>0x0447, 0x03A0=>0x03C0, 0x0418=>0x0438, 0x00D3=>0x00F3, + 0x0420=>0x0440, 0x0404=>0x0454, 0x0415=>0x0435, 0x0429=>0x0449, 0x014A=>0x014B, + 0x0411=>0x0431, 0x0409=>0x0459, 0x1E02=>0x1E03, 0x00D6=>0x00F6, 0x00D9=>0x00F9, + 0x004E=>0x006E, 0x0401=>0x0451, 0x03A4=>0x03C4, 0x0423=>0x0443, 0x015C=>0x015D, + 0x0403=>0x0453, 0x03A8=>0x03C8, 0x0158=>0x0159, 0x0047=>0x0067, 0x00C4=>0x00E4, + 0x0386=>0x03AC, 0x0389=>0x03AE, 0x0166=>0x0167, 0x039E=>0x03BE, 0x0164=>0x0165, + 0x0116=>0x0117, 0x0108=>0x0109, 0x0056=>0x0076, 0x00DE=>0x00FE, 0x0156=>0x0157, + 0x00DA=>0x00FA, 0x1E60=>0x1E61, 0x1E82=>0x1E83, 0x00C2=>0x00E2, 0x0118=>0x0119, + 0x0145=>0x0146, 0x0050=>0x0070, 0x0150=>0x0151, 0x042E=>0x044E, 0x0128=>0x0129, + 0x03A7=>0x03C7, 0x013D=>0x013E, 0x0422=>0x0442, 0x005A=>0x007A, 0x0428=>0x0448, + 0x03A1=>0x03C1, 0x1E80=>0x1E81, 0x016C=>0x016D, 0x00D5=>0x00F5, 0x0055=>0x0075, + 0x0176=>0x0177, 0x00DC=>0x00FC, 0x1E56=>0x1E57, 0x03A3=>0x03C3, 0x041A=>0x043A, + 0x004D=>0x006D, 0x016A=>0x016B, 0x0170=>0x0171, 0x0424=>0x0444, 0x00CC=>0x00EC, + 0x0168=>0x0169, 0x039F=>0x03BF, 0x004B=>0x006B, 0x00D2=>0x00F2, 0x00C0=>0x00E0, + 0x0414=>0x0434, 0x03A9=>0x03C9, 0x1E6A=>0x1E6B, 0x00C3=>0x00E3, 0x042D=>0x044D, + 0x0416=>0x0436, 0x01A0=>0x01A1, 0x010C=>0x010D, 0x011C=>0x011D, 0x00D0=>0x00F0, + 0x013B=>0x013C, 0x040F=>0x045F, 0x040A=>0x045A, 0x00C8=>0x00E8, 0x03A5=>0x03C5, + 0x0046=>0x0066, 0x00DD=>0x00FD, 0x0043=>0x0063, 0x021A=>0x021B, 0x00CA=>0x00EA, + 0x0399=>0x03B9, 0x0179=>0x017A, 0x00CF=>0x00EF, 0x01AF=>0x01B0, 0x0045=>0x0065, + 0x039B=>0x03BB, 0x0398=>0x03B8, 0x039C=>0x03BC, 0x040C=>0x045C, 0x041F=>0x043F, + 0x042C=>0x044C, 0x00DE=>0x00FE, 0x00D0=>0x00F0, 0x1EF2=>0x1EF3, 0x0048=>0x0068, + 0x00CB=>0x00EB, 0x0110=>0x0111, 0x0413=>0x0433, 0x012E=>0x012F, 0x00C6=>0x00E6, + 0x0058=>0x0078, 0x0160=>0x0161, 0x016E=>0x016F, 0x0391=>0x03B1, 0x0407=>0x0457, + 0x0172=>0x0173, 0x0178=>0x00FF, 0x004F=>0x006F, 0x041B=>0x043B, 0x0395=>0x03B5, + 0x0425=>0x0445, 0x0120=>0x0121, 0x017D=>0x017E, 0x017B=>0x017C, 0x0396=>0x03B6, + 0x0392=>0x03B2, 0x0388=>0x03AD, 0x1E84=>0x1E85, 0x0174=>0x0175, 0x0051=>0x0071, + 0x0417=>0x0437, 0x1E0A=>0x1E0B, 0x0147=>0x0148, 0x0104=>0x0105, 0x0408=>0x0458, + 0x014C=>0x014D, 0x00CD=>0x00ED, 0x0059=>0x0079, 0x010A=>0x010B, 0x038F=>0x03CE, + 0x0052=>0x0072, 0x0410=>0x0430, 0x0405=>0x0455, 0x0402=>0x0452, 0x0126=>0x0127, + 0x0136=>0x0137, 0x012A=>0x012B, 0x038A=>0x03AF, 0x042B=>0x044B, 0x004C=>0x006C, + 0x0397=>0x03B7, 0x0124=>0x0125, 0x0218=>0x0219, 0x00DB=>0x00FB, 0x011E=>0x011F, + 0x041E=>0x043E, 0x1E40=>0x1E41, 0x039D=>0x03BD, 0x0106=>0x0107, 0x03AB=>0x03CB, + 0x0426=>0x0446, 0x00DE=>0x00FE, 0x00C7=>0x00E7, 0x03AA=>0x03CA, 0x0421=>0x0441, + 0x0412=>0x0432, 0x010E=>0x010F, 0x00D8=>0x00F8, 0x0057=>0x0077, 0x011A=>0x011B, + 0x0054=>0x0074, 0x004A=>0x006A, 0x040B=>0x045B, 0x0406=>0x0456, 0x0102=>0x0103, + 0x039B=>0x03BB, 0x00D1=>0x00F1, 0x041D=>0x043D, 0x038C=>0x03CC, 0x00C9=>0x00E9, + 0x00D0=>0x00F0, 0x0407=>0x0457, 0x0122=>0x0123, + ); + } + + $uni = UTF8::to_unicode($str); + + if ($uni === FALSE) + return FALSE; + + for ($i = 0, $c = count($uni); $i < $c; $i++) + { + if (isset($UTF8_UPPER_TO_LOWER[$uni[$i]])) + { + $uni[$i] = $UTF8_UPPER_TO_LOWER[$uni[$i]]; + } + } + + return UTF8::from_unicode($uni); +} \ No newline at end of file diff --git a/includes/kohana/system/utf8/strtoupper.php b/includes/kohana/system/utf8/strtoupper.php new file mode 100644 index 00000000..85c2f923 --- /dev/null +++ b/includes/kohana/system/utf8/strtoupper.php @@ -0,0 +1,81 @@ +0x0041, 0x03C6=>0x03A6, 0x0163=>0x0162, 0x00E5=>0x00C5, 0x0062=>0x0042, + 0x013A=>0x0139, 0x00E1=>0x00C1, 0x0142=>0x0141, 0x03CD=>0x038E, 0x0101=>0x0100, + 0x0491=>0x0490, 0x03B4=>0x0394, 0x015B=>0x015A, 0x0064=>0x0044, 0x03B3=>0x0393, + 0x00F4=>0x00D4, 0x044A=>0x042A, 0x0439=>0x0419, 0x0113=>0x0112, 0x043C=>0x041C, + 0x015F=>0x015E, 0x0144=>0x0143, 0x00EE=>0x00CE, 0x045E=>0x040E, 0x044F=>0x042F, + 0x03BA=>0x039A, 0x0155=>0x0154, 0x0069=>0x0049, 0x0073=>0x0053, 0x1E1F=>0x1E1E, + 0x0135=>0x0134, 0x0447=>0x0427, 0x03C0=>0x03A0, 0x0438=>0x0418, 0x00F3=>0x00D3, + 0x0440=>0x0420, 0x0454=>0x0404, 0x0435=>0x0415, 0x0449=>0x0429, 0x014B=>0x014A, + 0x0431=>0x0411, 0x0459=>0x0409, 0x1E03=>0x1E02, 0x00F6=>0x00D6, 0x00F9=>0x00D9, + 0x006E=>0x004E, 0x0451=>0x0401, 0x03C4=>0x03A4, 0x0443=>0x0423, 0x015D=>0x015C, + 0x0453=>0x0403, 0x03C8=>0x03A8, 0x0159=>0x0158, 0x0067=>0x0047, 0x00E4=>0x00C4, + 0x03AC=>0x0386, 0x03AE=>0x0389, 0x0167=>0x0166, 0x03BE=>0x039E, 0x0165=>0x0164, + 0x0117=>0x0116, 0x0109=>0x0108, 0x0076=>0x0056, 0x00FE=>0x00DE, 0x0157=>0x0156, + 0x00FA=>0x00DA, 0x1E61=>0x1E60, 0x1E83=>0x1E82, 0x00E2=>0x00C2, 0x0119=>0x0118, + 0x0146=>0x0145, 0x0070=>0x0050, 0x0151=>0x0150, 0x044E=>0x042E, 0x0129=>0x0128, + 0x03C7=>0x03A7, 0x013E=>0x013D, 0x0442=>0x0422, 0x007A=>0x005A, 0x0448=>0x0428, + 0x03C1=>0x03A1, 0x1E81=>0x1E80, 0x016D=>0x016C, 0x00F5=>0x00D5, 0x0075=>0x0055, + 0x0177=>0x0176, 0x00FC=>0x00DC, 0x1E57=>0x1E56, 0x03C3=>0x03A3, 0x043A=>0x041A, + 0x006D=>0x004D, 0x016B=>0x016A, 0x0171=>0x0170, 0x0444=>0x0424, 0x00EC=>0x00CC, + 0x0169=>0x0168, 0x03BF=>0x039F, 0x006B=>0x004B, 0x00F2=>0x00D2, 0x00E0=>0x00C0, + 0x0434=>0x0414, 0x03C9=>0x03A9, 0x1E6B=>0x1E6A, 0x00E3=>0x00C3, 0x044D=>0x042D, + 0x0436=>0x0416, 0x01A1=>0x01A0, 0x010D=>0x010C, 0x011D=>0x011C, 0x00F0=>0x00D0, + 0x013C=>0x013B, 0x045F=>0x040F, 0x045A=>0x040A, 0x00E8=>0x00C8, 0x03C5=>0x03A5, + 0x0066=>0x0046, 0x00FD=>0x00DD, 0x0063=>0x0043, 0x021B=>0x021A, 0x00EA=>0x00CA, + 0x03B9=>0x0399, 0x017A=>0x0179, 0x00EF=>0x00CF, 0x01B0=>0x01AF, 0x0065=>0x0045, + 0x03BB=>0x039B, 0x03B8=>0x0398, 0x03BC=>0x039C, 0x045C=>0x040C, 0x043F=>0x041F, + 0x044C=>0x042C, 0x00FE=>0x00DE, 0x00F0=>0x00D0, 0x1EF3=>0x1EF2, 0x0068=>0x0048, + 0x00EB=>0x00CB, 0x0111=>0x0110, 0x0433=>0x0413, 0x012F=>0x012E, 0x00E6=>0x00C6, + 0x0078=>0x0058, 0x0161=>0x0160, 0x016F=>0x016E, 0x03B1=>0x0391, 0x0457=>0x0407, + 0x0173=>0x0172, 0x00FF=>0x0178, 0x006F=>0x004F, 0x043B=>0x041B, 0x03B5=>0x0395, + 0x0445=>0x0425, 0x0121=>0x0120, 0x017E=>0x017D, 0x017C=>0x017B, 0x03B6=>0x0396, + 0x03B2=>0x0392, 0x03AD=>0x0388, 0x1E85=>0x1E84, 0x0175=>0x0174, 0x0071=>0x0051, + 0x0437=>0x0417, 0x1E0B=>0x1E0A, 0x0148=>0x0147, 0x0105=>0x0104, 0x0458=>0x0408, + 0x014D=>0x014C, 0x00ED=>0x00CD, 0x0079=>0x0059, 0x010B=>0x010A, 0x03CE=>0x038F, + 0x0072=>0x0052, 0x0430=>0x0410, 0x0455=>0x0405, 0x0452=>0x0402, 0x0127=>0x0126, + 0x0137=>0x0136, 0x012B=>0x012A, 0x03AF=>0x038A, 0x044B=>0x042B, 0x006C=>0x004C, + 0x03B7=>0x0397, 0x0125=>0x0124, 0x0219=>0x0218, 0x00FB=>0x00DB, 0x011F=>0x011E, + 0x043E=>0x041E, 0x1E41=>0x1E40, 0x03BD=>0x039D, 0x0107=>0x0106, 0x03CB=>0x03AB, + 0x0446=>0x0426, 0x00FE=>0x00DE, 0x00E7=>0x00C7, 0x03CA=>0x03AA, 0x0441=>0x0421, + 0x0432=>0x0412, 0x010F=>0x010E, 0x00F8=>0x00D8, 0x0077=>0x0057, 0x011B=>0x011A, + 0x0074=>0x0054, 0x006A=>0x004A, 0x045B=>0x040B, 0x0456=>0x0406, 0x0103=>0x0102, + 0x03BB=>0x039B, 0x00F1=>0x00D1, 0x043D=>0x041D, 0x03CC=>0x038C, 0x00E9=>0x00C9, + 0x00F0=>0x00D0, 0x0457=>0x0407, 0x0123=>0x0122, + ); + } + + $uni = UTF8::to_unicode($str); + + if ($uni === FALSE) + return FALSE; + + for ($i = 0, $c = count($uni); $i < $c; $i++) + { + if (isset($UTF8_LOWER_TO_UPPER[$uni[$i]])) + { + $uni[$i] = $UTF8_LOWER_TO_UPPER[$uni[$i]]; + } + } + + return UTF8::from_unicode($uni); +} \ No newline at end of file diff --git a/includes/kohana/system/utf8/substr.php b/includes/kohana/system/utf8/substr.php new file mode 100644 index 00000000..c76b89b9 --- /dev/null +++ b/includes/kohana/system/utf8/substr.php @@ -0,0 +1,72 @@ += $strlen OR ($length < 0 AND $length <= $offset - $strlen)) + return ''; + + // Whole string + if ($offset == 0 AND ($length === NULL OR $length >= $strlen)) + return $str; + + // Build regex + $regex = '^'; + + // Create an offset expression + if ($offset > 0) + { + // PCRE repeating quantifiers must be less than 65536, so repeat when necessary + $x = (int) ($offset / 65535); + $y = (int) ($offset % 65535); + $regex .= ($x == 0) ? '' : '(?:.{65535}){'.$x.'}'; + $regex .= ($y == 0) ? '' : '.{'.$y.'}'; + } + + // Create a length expression + if ($length === NULL) + { + $regex .= '(.*)'; // No length set, grab it all + } + // Find length from the left (positive length) + elseif ($length > 0) + { + // Reduce length so that it can't go beyond the end of the string + $length = min($strlen - $offset, $length); + + $x = (int) ($length / 65535); + $y = (int) ($length % 65535); + $regex .= '('; + $regex .= ($x == 0) ? '' : '(?:.{65535}){'.$x.'}'; + $regex .= '.{'.$y.'})'; + } + // Find length from the right (negative length) + else + { + $x = (int) (-$length / 65535); + $y = (int) (-$length % 65535); + $regex .= '(.*)'; + $regex .= ($x == 0) ? '' : '(?:.{65535}){'.$x.'}'; + $regex .= '.{'.$y.'}'; + } + + preg_match('/'.$regex.'/us', $str, $matches); + return $matches[1]; +} \ No newline at end of file diff --git a/includes/kohana/system/utf8/substr_replace.php b/includes/kohana/system/utf8/substr_replace.php new file mode 100644 index 00000000..b051842d --- /dev/null +++ b/includes/kohana/system/utf8/substr_replace.php @@ -0,0 +1,22 @@ + 0x10FFFF)) + { + trigger_error('UTF8::to_unicode: Illegal sequence or codepoint in UTF-8 at byte '.$i, E_USER_WARNING); + return FALSE; + } + + if (0xFEFF != $mUcs4) + { + // BOM is legal but we don't want to output it + $out[] = $mUcs4; + } + + // Initialize UTF-8 cache + $mState = 0; + $mUcs4 = 0; + $mBytes = 1; + } + } + else + { + // ((0xC0 & (*in) != 0x80) AND (mState != 0)) + // Incomplete multi-octet sequence + trigger_error('UTF8::to_unicode: Incomplete multi-octet sequence in UTF-8 at byte '.$i, E_USER_WARNING); + return FALSE; + } + } + } + + return $out; +} \ No newline at end of file diff --git a/includes/kohana/system/utf8/transliterate_to_ascii.php b/includes/kohana/system/utf8/transliterate_to_ascii.php new file mode 100644 index 00000000..a48d6b00 --- /dev/null +++ b/includes/kohana/system/utf8/transliterate_to_ascii.php @@ -0,0 +1,77 @@ + 'a', 'ô' => 'o', 'ď' => 'd', 'ḟ' => 'f', 'ë' => 'e', 'š' => 's', 'ơ' => 'o', + 'ß' => 'ss', 'ă' => 'a', 'ř' => 'r', 'ț' => 't', 'ň' => 'n', 'ā' => 'a', 'ķ' => 'k', + 'ŝ' => 's', 'ỳ' => 'y', 'ņ' => 'n', 'ĺ' => 'l', 'ħ' => 'h', 'ṗ' => 'p', 'ó' => 'o', + 'ú' => 'u', 'ě' => 'e', 'é' => 'e', 'ç' => 'c', 'ẁ' => 'w', 'ċ' => 'c', 'õ' => 'o', + 'ṡ' => 's', 'ø' => 'o', 'ģ' => 'g', 'ŧ' => 't', 'ș' => 's', 'ė' => 'e', 'ĉ' => 'c', + 'ś' => 's', 'î' => 'i', 'ű' => 'u', 'ć' => 'c', 'ę' => 'e', 'ŵ' => 'w', 'ṫ' => 't', + 'ū' => 'u', 'č' => 'c', 'ö' => 'o', 'è' => 'e', 'ŷ' => 'y', 'ą' => 'a', 'ł' => 'l', + 'ų' => 'u', 'ů' => 'u', 'ş' => 's', 'ğ' => 'g', 'ļ' => 'l', 'ƒ' => 'f', 'ž' => 'z', + 'ẃ' => 'w', 'ḃ' => 'b', 'å' => 'a', 'ì' => 'i', 'ï' => 'i', 'ḋ' => 'd', 'ť' => 't', + 'ŗ' => 'r', 'ä' => 'a', 'í' => 'i', 'ŕ' => 'r', 'ê' => 'e', 'ü' => 'u', 'ò' => 'o', + 'ē' => 'e', 'ñ' => 'n', 'ń' => 'n', 'ĥ' => 'h', 'ĝ' => 'g', 'đ' => 'd', 'ĵ' => 'j', + 'ÿ' => 'y', 'ũ' => 'u', 'ŭ' => 'u', 'ư' => 'u', 'ţ' => 't', 'ý' => 'y', 'ő' => 'o', + 'â' => 'a', 'ľ' => 'l', 'ẅ' => 'w', 'ż' => 'z', 'ī' => 'i', 'ã' => 'a', 'ġ' => 'g', + 'ṁ' => 'm', 'ō' => 'o', 'ĩ' => 'i', 'ù' => 'u', 'į' => 'i', 'ź' => 'z', 'á' => 'a', + 'û' => 'u', 'þ' => 'th', 'ð' => 'dh', 'æ' => 'ae', 'µ' => 'u', 'ĕ' => 'e', 'ı' => 'i', + ); + } + + $str = str_replace( + array_keys($UTF8_LOWER_ACCENTS), + array_values($UTF8_LOWER_ACCENTS), + $str + ); + } + + if ($case >= 0) + { + if ($UTF8_UPPER_ACCENTS === NULL) + { + $UTF8_UPPER_ACCENTS = array( + 'À' => 'A', 'Ô' => 'O', 'Ď' => 'D', 'Ḟ' => 'F', 'Ë' => 'E', 'Š' => 'S', 'Ơ' => 'O', + 'Ă' => 'A', 'Ř' => 'R', 'Ț' => 'T', 'Ň' => 'N', 'Ā' => 'A', 'Ķ' => 'K', 'Ĕ' => 'E', + 'Ŝ' => 'S', 'Ỳ' => 'Y', 'Ņ' => 'N', 'Ĺ' => 'L', 'Ħ' => 'H', 'Ṗ' => 'P', 'Ó' => 'O', + 'Ú' => 'U', 'Ě' => 'E', 'É' => 'E', 'Ç' => 'C', 'Ẁ' => 'W', 'Ċ' => 'C', 'Õ' => 'O', + 'Ṡ' => 'S', 'Ø' => 'O', 'Ģ' => 'G', 'Ŧ' => 'T', 'Ș' => 'S', 'Ė' => 'E', 'Ĉ' => 'C', + 'Ś' => 'S', 'Î' => 'I', 'Ű' => 'U', 'Ć' => 'C', 'Ę' => 'E', 'Ŵ' => 'W', 'Ṫ' => 'T', + 'Ū' => 'U', 'Č' => 'C', 'Ö' => 'O', 'È' => 'E', 'Ŷ' => 'Y', 'Ą' => 'A', 'Ł' => 'L', + 'Ų' => 'U', 'Ů' => 'U', 'Ş' => 'S', 'Ğ' => 'G', 'Ļ' => 'L', 'Ƒ' => 'F', 'Ž' => 'Z', + 'Ẃ' => 'W', 'Ḃ' => 'B', 'Å' => 'A', 'Ì' => 'I', 'Ï' => 'I', 'Ḋ' => 'D', 'Ť' => 'T', + 'Ŗ' => 'R', 'Ä' => 'A', 'Í' => 'I', 'Ŕ' => 'R', 'Ê' => 'E', 'Ü' => 'U', 'Ò' => 'O', + 'Ē' => 'E', 'Ñ' => 'N', 'Ń' => 'N', 'Ĥ' => 'H', 'Ĝ' => 'G', 'Đ' => 'D', 'Ĵ' => 'J', + 'Ÿ' => 'Y', 'Ũ' => 'U', 'Ŭ' => 'U', 'Ư' => 'U', 'Ţ' => 'T', 'Ý' => 'Y', 'Ő' => 'O', + 'Â' => 'A', 'Ľ' => 'L', 'Ẅ' => 'W', 'Ż' => 'Z', 'Ī' => 'I', 'Ã' => 'A', 'Ġ' => 'G', + 'Ṁ' => 'M', 'Ō' => 'O', 'Ĩ' => 'I', 'Ù' => 'U', 'Į' => 'I', 'Ź' => 'Z', 'Á' => 'A', + 'Û' => 'U', 'Þ' => 'Th', 'Ð' => 'Dh', 'Æ' => 'Ae', 'İ' => 'I', + ); + } + + $str = str_replace( + array_keys($UTF8_UPPER_ACCENTS), + array_values($UTF8_UPPER_ACCENTS), + $str + ); + } + + return $str; +} \ No newline at end of file diff --git a/includes/kohana/system/utf8/trim.php b/includes/kohana/system/utf8/trim.php new file mode 100644 index 00000000..374c0c1a --- /dev/null +++ b/includes/kohana/system/utf8/trim.php @@ -0,0 +1,17 @@ + + + +
            +

            [ ]:

            +
            +

            [ ]

            + +
              + $step): ?> +
            1. +

              + + + [ ] + + {} + + + » + () +

              + + + + + + +
            2. + + +
            +
            +

            + +
            diff --git a/includes/kohana/system/views/kohana/generate_logo.php b/includes/kohana/system/views/kohana/generate_logo.php new file mode 100644 index 00000000..5960266e --- /dev/null +++ b/includes/kohana/system/views/kohana/generate_logo.php @@ -0,0 +1,14 @@ + 'image/png', 'data' => '{$data}'); ?>"); \ No newline at end of file diff --git a/includes/kohana/system/views/kohana/logo.php b/includes/kohana/system/views/kohana/logo.php new file mode 100644 index 00000000..d743eaa5 --- /dev/null +++ b/includes/kohana/system/views/kohana/logo.php @@ -0,0 +1,8 @@ + 'image/png', 'data' => 'iVBORw0KGgoAAAANSUhEUgAAAL8AAAA+CAYAAAB6Bsp7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAACtppVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDQuMi4yLWMwNjMgNTMuMzUyNjI0LCAyMDA4LzA3LzMwLTE4OjA1OjQxICAgICAgICAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICAgeG1sbnM6eG1wUmlnaHRzPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvcmlnaHRzLyIKICAgeG1wUmlnaHRzOk1hcmtlZD0iRmFsc2UiCiAgIHhtcFJpZ2h0czpXZWJTdGF0ZW1lbnQ9IiI+CiAgIDxkYzpyaWdodHM+CiAgICA8cmRmOkFsdD4KICAgICA8cmRmOmxpIHhtbDpsYW5nPSJ4LWRlZmF1bHQiLz4KICAgIDwvcmRmOkFsdD4KICAgPC9kYzpyaWdodHM+CiAgIDx4bXBSaWdodHM6VXNhZ2VUZXJtcz4KICAgIDxyZGY6QWx0PgogICAgIDxyZGY6bGkgeG1sOmxhbmc9IngtZGVmYXVsdCIvPgogICAgPC9yZGY6QWx0PgogICA8L3htcFJpZ2h0czpVc2FnZVRlcm1zPgogIDwvcmRmOkRlc2NyaXB0aW9uPgogPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg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'); ?> \ No newline at end of file diff --git a/includes/kohana/system/views/profiler/stats.php b/includes/kohana/system/views/profiler/stats.php new file mode 100755 index 00000000..ebfb489b --- /dev/null +++ b/includes/kohana/system/views/profiler/stats.php @@ -0,0 +1,74 @@ + + + + + + +
            + $benchmarks): ?> + + + + + + + + + + + + + + + $tokens): ?> + + + + + + + + + + + + + +
            s
            kB
            +
            +
            s
            + +
            + +
            +
            +
            +
            kB
            + +
            + +
            +
            + + + + + + + + + + + + + + + +
            s
            kB
            +
            \ No newline at end of file diff --git a/includes/kohana/system/views/profiler/style.css b/includes/kohana/system/views/profiler/style.css new file mode 100644 index 00000000..e6af3a05 --- /dev/null +++ b/includes/kohana/system/views/profiler/style.css @@ -0,0 +1,27 @@ +.kohana table.profiler { width: 99%; margin: 0 auto 1em; border-collapse: collapse; } +.kohana table.profiler th, +.kohana table.profiler td { padding: 0.2em 0.4em; background: #fff; border: solid 1px #999; border-width: 1px 0; text-align: left; font-weight: normal; font-size: 1em; color: #111; vertical-align: top; text-align: right; } +.kohana table.profiler th.name { text-align: left; } +.kohana table.profiler tr.group th { font-size: 1.4em; background: #222; color: #eee; border-color: #222; } +.kohana table.profiler tr.group td { background: #222; color: #777; border-color: #222; } +.kohana table.profiler tr.group td.time { padding-bottom: 0; } +.kohana table.profiler tr.headers th { text-transform: lowercase; font-variant: small-caps; background: #ddd; color: #777; } +.kohana table.profiler tr.mark th.name { width: 40%; font-size: 1.2em; background: #fff; vertical-align: middle; } +.kohana table.profiler tr.mark td { padding: 0; } +.kohana table.profiler tr.mark.final td { padding: 0.2em 0.4em; } +.kohana table.profiler tr.mark td > div { position: relative; padding: 0.2em 0.4em; } +.kohana table.profiler tr.mark td div.value { position: relative; z-index: 2; } +.kohana table.profiler tr.mark td div.graph { position: absolute; top: 0; bottom: 0; right: 0; left: 100%; background: #71bdf0; z-index: 1; } +.kohana table.profiler tr.mark.memory td div.graph { background: #acd4f0; } +.kohana table.profiler tr.mark td.current { background: #eddecc; } +.kohana table.profiler tr.mark td.min { background: #d2f1cb; } +.kohana table.profiler tr.mark td.max { background: #ead3cb; } +.kohana table.profiler tr.mark td.average { background: #ddd; } +.kohana table.profiler tr.mark td.total { background: #d0e3f0; } +.kohana table.profiler tr.time td { border-bottom: 0; font-weight: bold; } +.kohana table.profiler tr.memory td { border-top: 0; } +.kohana table.profiler tr.final th.name { background: #222; color: #fff; } +.kohana table.profiler abbr { border: 0; color: #777; font-weight: normal; } +.kohana table.profiler:hover tr.group td { color: #ccc; } +.kohana table.profiler:hover tr.mark td div.graph { background: #1197f0; } +.kohana table.profiler:hover tr.mark.memory td div.graph { background: #7cc1f0; } \ No newline at end of file diff --git a/kh.php b/kh.php new file mode 100644 index 00000000..c6b09632 --- /dev/null +++ b/kh.php @@ -0,0 +1,103 @@ += 5.3, it is recommended to disable + * deprecated notices. Disable with: E_ALL & ~E_DEPRECATED + */ +error_reporting(E_ALL | E_STRICT); + +/** + * End of standard configuration! Changing any of the code below should only be + * attempted by those with a working knowledge of Kohana internals. + * + * @see http://kohanaframework.org/guide/using.configuration + */ + +// Set the full path to the docroot +define('DOCROOT', realpath(dirname(__FILE__)).DIRECTORY_SEPARATOR); + +// Make the application relative to the docroot +if ( ! is_dir($application) AND is_dir(DOCROOT.$application)) + $application = DOCROOT.$application; + +// Make the modules relative to the docroot +if ( ! is_dir($modules) AND is_dir(DOCROOT.$modules)) + $modules = DOCROOT.$modules; + +// Make the system relative to the docroot +if ( ! is_dir($system) AND is_dir(DOCROOT.$system)) + $system = DOCROOT.$system; + +// Define the absolute paths for configured directories +define('APPPATH', realpath($application).DIRECTORY_SEPARATOR); +define('MODPATH', realpath($modules).DIRECTORY_SEPARATOR); +define('SYSPATH', realpath($system).DIRECTORY_SEPARATOR); + +// Clean up the configuration vars +unset($application, $modules, $system); + +if (file_exists('install'.EXT)) +{ + // Load the installation check + return include 'install'.EXT; +} + +// Load the base, low-level functions +require SYSPATH.'base'.EXT; + +// Load the core Kohana class +require SYSPATH.'classes/kohana/core'.EXT; + +if (is_file(APPPATH.'classes/kohana'.EXT)) +{ + // Application extends the core + require APPPATH.'classes/kohana'.EXT; +} +else +{ + // Load empty core extension + require SYSPATH.'classes/kohana'.EXT; +} + +// Bootstrap the application +require APPPATH.'bootstrap'.EXT;